diff --git a/.github/workflows/auto-approve-v2-merge-forward.yml b/.github/workflows/auto-approve-v2-merge-forward.yml deleted file mode 100644 index f05cd6753316c..0000000000000 --- a/.github/workflows/auto-approve-v2-merge-forward.yml +++ /dev/null @@ -1,26 +0,0 @@ -# Automatically approve PRs that merge master forward to v2-main -# -# Only does approvals! mergify takes care of the actual merge. -name: Auto-approve automated PRs around CDK v2 -on: - pull_request: - types: - - labeled - - opened - - ready_for_review - - reopened - - synchronize - - unlabeled - - unlocked - -jobs: - approve: - runs-on: ubuntu-latest - steps: - - uses: hmarr/auto-approve-action@v2.0.0 - if: > - github.event.pull_request.user.login == 'aws-cdk-automation' - && github.event.pull_request.base.ref == 'v2-main' - && contains(github.event.pull_request.labels.*.name, 'pr/auto-approve') - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/v2-pull-request.yml b/.github/workflows/v2-pull-request.yml new file mode 100644 index 0000000000000..c4118d3298a00 --- /dev/null +++ b/.github/workflows/v2-pull-request.yml @@ -0,0 +1,46 @@ +# Automated actions for PRs against the v2-main branch +name: v2 +on: + pull_request: + branches: + - v2-main + types: + - labeled + - opened + - ready_for_review + - reopened + - synchronize + - unlabeled + - unlocked + +jobs: + # Run yarn pkglint on merge forward PRs and commit the results + pkglint: + if: contains(github.event.pull_request.labels.*.name, 'pr/forward-merge') + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v2 + with: + branch: ${{ github.event.pull_request.head.ref }} + token: ${{ secrets.GITHUB_TOKEN }} + - name: lint + run: |- + yarn install --frozen-lockfile + yarn pkglint + - name: push + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: 'automatic pkglint fixes' + + # Approve automated PRs + # Only approve! mergify takes care of the actual merge. + auto-approve: + if: > + github.event.pull_request.user.login == 'aws-cdk-automation' + && contains(github.event.pull_request.labels.*.name, 'pr/auto-approve') + runs-on: ubuntu-latest + steps: + - uses: hmarr/auto-approve-action@v2.0.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/yarn-upgrade.yml b/.github/workflows/yarn-upgrade.yml index 586e7f2c9357a..6734719a67184 100644 --- a/.github/workflows/yarn-upgrade.yml +++ b/.github/workflows/yarn-upgrade.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Node - uses: actions/setup-node@v2.1.0 + uses: actions/setup-node@v2.1.4 with: node-version: 10 @@ -55,12 +55,15 @@ jobs: lerna exec --parallel ncu -- --upgrade --filter=@types/node,@types/fs-extra --target=minor lerna exec --parallel ncu -- --upgrade --filter=typescript --target=patch lerna exec --parallel ncu -- --upgrade --reject='@types/node,@types/fs-extra,constructs,typescript,aws-sdk,${{ steps.list-packages.outputs.list }}' --target=minor - # This will create a brand new `yarn.lock` file (this is more efficient than `yarn install && yarn upgrade`) - - name: Run "yarn install --force" - run: yarn install --force + # This will ensure the current lockfile is up-to-date with the dependency specifications (necessary for "yarn update" to run) + - name: Run "yarn install" + run: yarn install + + - name: Run "yarn upgrade" + run: yarn upgrade - name: Make Pull Request - uses: peter-evans/create-pull-request@v2 + uses: peter-evans/create-pull-request@v3 with: # Git commit details branch: automation/yarn-upgrade diff --git a/.yarnrc b/.yarnrc index 46241e3f5e5bc..591e9c3d57b96 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,2 +1 @@ --install.check-files true # install will verify file tree of packages for consistency -ignore-engines true # the 'engines' key for 'aws-cdk-lib' has specifies node14 as min while v1 will remain at node10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 53464a6ced7a9..e62bd18aecd9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,102 @@ 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.89.0](https://github.com/aws/aws-cdk/compare/v1.88.0...v1.89.0) (2021-02-09) + + +### Features + +* **cfnspec:** cloudformation spec v26.0.0 ([#12841](https://github.com/aws/aws-cdk/issues/12841)) ([f959b3a](https://github.com/aws/aws-cdk/commit/f959b3a2eeb5a9a9e44ea3f88622f77f7667bfa4)) +* **cloudfront:** add support for TrustedKeyGroups in Distribution and CloudFrontWebDistribution ([#12847](https://github.com/aws/aws-cdk/issues/12847)) ([349a6e2](https://github.com/aws/aws-cdk/commit/349a6e2bfaa72440deb3767fb1e28e38cc4d73ef)), closes [#11791](https://github.com/aws/aws-cdk/issues/11791) +* **core:** configure bundling docker entrypoint ([#12660](https://github.com/aws/aws-cdk/issues/12660)) ([6597a09](https://github.com/aws/aws-cdk/commit/6597a09310fbc13d43389eca91b0e4b26f8ca680)), closes [#11984](https://github.com/aws/aws-cdk/issues/11984) +* **ec2:** can define Launch Templates (not use them yet) ([#12385](https://github.com/aws/aws-cdk/issues/12385)) ([32c0de7](https://github.com/aws/aws-cdk/commit/32c0de74cf40f08a291c8589fd85f3dd636749ea)) +* **lambda:** layer version removal policy ([#12792](https://github.com/aws/aws-cdk/issues/12792)) ([5664480](https://github.com/aws/aws-cdk/commit/5664480a97958263ee7cb903c2aff0276e738dc3)), closes [#12718](https://github.com/aws/aws-cdk/issues/12718) +* **lambda:** nodejs14.x runtime ([#12861](https://github.com/aws/aws-cdk/issues/12861)) ([12c224a](https://github.com/aws/aws-cdk/commit/12c224a0f54230b6226de8defa527f7b53f9bc65)) + + +### Bug Fixes + +* **core:** append file extension to s3 asset key in new style synthesizer ([#12765](https://github.com/aws/aws-cdk/issues/12765)) ([77b9d39](https://github.com/aws/aws-cdk/commit/77b9d3930ec722be3a40e4013cd9335f90b0d945)), closes [#12740](https://github.com/aws/aws-cdk/issues/12740) +* **core:** incorrect GetParameter permissions in nonstandard partitions ([#12813](https://github.com/aws/aws-cdk/issues/12813)) ([be7202f](https://github.com/aws/aws-cdk/commit/be7202fa229435607e81d480726e9ce7f625b85a)) +* **ec2:** MachineImage.genericLinux/Windows don't work in environment-agnostic stacks ([#12546](https://github.com/aws/aws-cdk/issues/12546)) ([fbe7e89](https://github.com/aws/aws-cdk/commit/fbe7e89ba764093ddec9caa7de3ca921f3dc68ac)), closes [#8759](https://github.com/aws/aws-cdk/issues/8759) +* **ec2:** Subnet cidr missing for Vpc.from_lookup() ([#12878](https://github.com/aws/aws-cdk/issues/12878)) ([9028269](https://github.com/aws/aws-cdk/commit/90282693999efdc43330b9526b9d7f4cd0fa5736)), closes [#11821](https://github.com/aws/aws-cdk/issues/11821) +* **ec2:** VpnConnection fails if `ip` is a Token ([#12923](https://github.com/aws/aws-cdk/issues/12923)) ([953957a](https://github.com/aws/aws-cdk/commit/953957a2c3e630b5ad2196e113f943e27ee21067)), closes [#11633](https://github.com/aws/aws-cdk/issues/11633) +* **kms:** cross-environment usage fails when trustAccountIdentities is set ([#12925](https://github.com/aws/aws-cdk/issues/12925)) ([2b917ec](https://github.com/aws/aws-cdk/commit/2b917eceb598b3365123781445df7e2bd8a80b74)), closes [#12921](https://github.com/aws/aws-cdk/issues/12921) [#12741](https://github.com/aws/aws-cdk/issues/12741) +* **lambda-python:** cryptography >= 3.4 is not supported by older pip version ([#12934](https://github.com/aws/aws-cdk/issues/12934)) ([b68acf8](https://github.com/aws/aws-cdk/commit/b68acf828e04841dd7e62b30fe80db8c25e5d96e)), closes [/cryptography.io/en/3.4/changelog.html#v3-4](https://github.com/aws//cryptography.io/en/3.4/changelog.html/issues/v3-4) + +## [1.88.0](https://github.com/aws/aws-cdk/compare/v1.87.1...v1.88.0) (2021-02-03) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **appmesh:** the properties virtualRouter and virtualNode of VirtualServiceProps have been replaced with the union-like class VirtualServiceProvider +* **appmesh**: the method `addVirtualService` has been removed from `IMesh` +* **cloudfront:** experimental EdgeFunction stack names have changed from 'edge-lambda-stack-${region}' to 'edge-lambda-stack-${stackid}' to support multiple independent CloudFront distributions with EdgeFunctions. + +### Features + +* **apigateway:** cognito user pool authorizer ([#12786](https://github.com/aws/aws-cdk/issues/12786)) ([ff1e5b3](https://github.com/aws/aws-cdk/commit/ff1e5b3c580119c107fe26c67fe3cc220f9ee7c9)), closes [#5618](https://github.com/aws/aws-cdk/issues/5618) +* **apigateway:** import an existing Resource ([#12785](https://github.com/aws/aws-cdk/issues/12785)) ([8a1a9b8](https://github.com/aws/aws-cdk/commit/8a1a9b82a36e681334fd45be595f6ecdf904ad34)), closes [#4432](https://github.com/aws/aws-cdk/issues/4432) +* **appmesh:** change VirtualService provider to a union-like class ([#11978](https://github.com/aws/aws-cdk/issues/11978)) ([dfc765a](https://github.com/aws/aws-cdk/commit/dfc765af44c755f10be8f6c1c2eae55f62e2aa08)), closes [#9490](https://github.com/aws/aws-cdk/issues/9490) +* **aws-route53:** cross account DNS delegations ([#12680](https://github.com/aws/aws-cdk/issues/12680)) ([126a693](https://github.com/aws/aws-cdk/commit/126a6935cacc1f68b1d1155e484912d4ed6978f2)), closes [#8776](https://github.com/aws/aws-cdk/issues/8776) +* **cloudfront:** add PublicKey and KeyGroup L2 constructs ([#12743](https://github.com/aws/aws-cdk/issues/12743)) ([59cb6d0](https://github.com/aws/aws-cdk/commit/59cb6d032a55515ec5e9903f899de588d18d4cb5)) +* **core:** `stack.exportValue()` can be used to solve "deadly embrace" ([#12778](https://github.com/aws/aws-cdk/issues/12778)) ([3b66088](https://github.com/aws/aws-cdk/commit/3b66088010b6f2315a215e92505d5279680f16d4)), closes [#7602](https://github.com/aws/aws-cdk/issues/7602) [#2036](https://github.com/aws/aws-cdk/issues/2036) +* **ecr:** Public Gallery authorization token ([#12775](https://github.com/aws/aws-cdk/issues/12775)) ([8434294](https://github.com/aws/aws-cdk/commit/84342943ad9f2ea8a83773f00816a0b8117c4d17)) +* **ecs-patterns:** Add PlatformVersion option to ScheduledFargateTask props ([#12676](https://github.com/aws/aws-cdk/issues/12676)) ([3cbf38b](https://github.com/aws/aws-cdk/commit/3cbf38b09a9e66a6c009f833481fb25b8c5fc26c)), closes [#12623](https://github.com/aws/aws-cdk/issues/12623) +* **elbv2:** support for 2020 SSL policy ([#12710](https://github.com/aws/aws-cdk/issues/12710)) ([1dd3d05](https://github.com/aws/aws-cdk/commit/1dd3d0518dc2a70c725f87dd5d4377338389125c)), closes [#12595](https://github.com/aws/aws-cdk/issues/12595) +* **iam:** Permissions Boundaries ([#12777](https://github.com/aws/aws-cdk/issues/12777)) ([415eb86](https://github.com/aws/aws-cdk/commit/415eb861c65829cc53eabbbb8706f83f08c74570)), closes [aws/aws-cdk-rfcs#5](https://github.com/aws/aws-cdk-rfcs/issues/5) [#3242](https://github.com/aws/aws-cdk/issues/3242) +* **lambda:** inline code for Python 3.8 ([#12788](https://github.com/aws/aws-cdk/issues/12788)) ([8d3aaba](https://github.com/aws/aws-cdk/commit/8d3aabaffe436e6a3eebc0a58fe361c5b4b93f08)), closes [#6503](https://github.com/aws/aws-cdk/issues/6503) + + +### Bug Fixes + +* **apigateway:** stack update fails to replace api key ([#12745](https://github.com/aws/aws-cdk/issues/12745)) ([ffe7e42](https://github.com/aws/aws-cdk/commit/ffe7e425e605144a465cea9befa68d4fe19f9d8c)), closes [#12698](https://github.com/aws/aws-cdk/issues/12698) +* **cfn-include:** AWS::CloudFormation resources fail in monocdk ([#12758](https://github.com/aws/aws-cdk/issues/12758)) ([5060782](https://github.com/aws/aws-cdk/commit/5060782b00e17bdf44e225f8f5ef03344be238c7)), closes [#11595](https://github.com/aws/aws-cdk/issues/11595) +* **cli, codepipeline:** renamed bootstrap stack still not supported ([#12771](https://github.com/aws/aws-cdk/issues/12771)) ([40b32bb](https://github.com/aws/aws-cdk/commit/40b32bbda272b6e2f92fd5dd8de7ca5bf405ce52)), closes [#12594](https://github.com/aws/aws-cdk/issues/12594) [#12732](https://github.com/aws/aws-cdk/issues/12732) +* **cloudfront:** use node addr for edgeStackId name ([#12702](https://github.com/aws/aws-cdk/issues/12702)) ([c429bb7](https://github.com/aws/aws-cdk/commit/c429bb7df2406346426dce22d716cabc484ec7e6)), closes [#12323](https://github.com/aws/aws-cdk/issues/12323) +* **codedeploy:** wrong syntax on Windows 'installAgent' flag ([#12736](https://github.com/aws/aws-cdk/issues/12736)) ([238742e](https://github.com/aws/aws-cdk/commit/238742e4323310ce850d8edc70abe4b0e9f53186)), closes [#12734](https://github.com/aws/aws-cdk/issues/12734) +* **codepipeline:** permission denied for Action-level environment variables ([#12761](https://github.com/aws/aws-cdk/issues/12761)) ([99fd074](https://github.com/aws/aws-cdk/commit/99fd074a07ead624f64d3fe64685ba67c798976e)), closes [#12742](https://github.com/aws/aws-cdk/issues/12742) +* **ec2:** ARM-backed bastion hosts try to run x86-based Amazon Linux AMI ([#12280](https://github.com/aws/aws-cdk/issues/12280)) ([1a73d76](https://github.com/aws/aws-cdk/commit/1a73d761ad2363842567a1b6e0488ceb093e70b2)), closes [#12279](https://github.com/aws/aws-cdk/issues/12279) +* **efs:** EFS fails to create when using a VPC with multiple subnets per availability zone ([#12097](https://github.com/aws/aws-cdk/issues/12097)) ([889d673](https://github.com/aws/aws-cdk/commit/889d6734c10174f2661e45057c345cd112a44187)), closes [#10170](https://github.com/aws/aws-cdk/issues/10170) +* **iam:** cannot use the same Role for multiple Config Rules ([#12724](https://github.com/aws/aws-cdk/issues/12724)) ([2f6521a](https://github.com/aws/aws-cdk/commit/2f6521a1d8670b2653f7dee281309351181cf918)), closes [#12714](https://github.com/aws/aws-cdk/issues/12714) +* **lambda:** codeguru profiler not set up for Node runtime ([#12712](https://github.com/aws/aws-cdk/issues/12712)) ([59db763](https://github.com/aws/aws-cdk/commit/59db763e7d05d68fd85b6fd37246d69d4670d7d5)), closes [#12624](https://github.com/aws/aws-cdk/issues/12624) + +## [1.87.1](https://github.com/aws/aws-cdk/compare/v1.87.0...v1.87.1) (2021-01-28) + + +### Bug Fixes + +* **apigateway:** stack update fails to replace api key ([38cbe62](https://github.com/aws/aws-cdk/commit/38cbe620859d6efabda95dbdd3185a480ab43894)), closes [#12698](https://github.com/aws/aws-cdk/issues/12698) + +## [1.87.0](https://github.com/aws/aws-cdk/compare/v1.86.0...v1.87.0) (2021-01-27) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **s3-deployment:** User metadata keys of bucket objects will change from `x-amz-meta-x-amz-meta-x-amzn-meta-mykey` to `x-amz-meta-mykey`. +* **core:** users of modern synthesis (`DefaultSynthesizer`, +used by CDK Pipelines) must upgrade their bootstrap stacks. Run `cdk bootstrap`. + +### Features + +* **aws-codebuild:** add `enableBatchBuilds()` to Project ([#12531](https://github.com/aws/aws-cdk/issues/12531)) ([0568390](https://github.com/aws/aws-cdk/commit/05683907d6ffc9ab12b6744c1b59b0df096789e1)) +* **aws-codepipeline-actions:** Add Full Clone support for CodeCommit ([#12558](https://github.com/aws/aws-cdk/issues/12558)) ([d169688](https://github.com/aws/aws-cdk/commit/d169688f35bc78c88c44ff9a7d8fa0dfea71f904)), closes [#12236](https://github.com/aws/aws-cdk/issues/12236) +* **batch:** Compute Resources placement group ([#12203](https://github.com/aws/aws-cdk/issues/12203)) ([fe37174](https://github.com/aws/aws-cdk/commit/fe37174ec29b7d3b60b252df08ceecf1aa057098)) +* **eks:** Graduate to stable ([#12640](https://github.com/aws/aws-cdk/issues/12640)) ([b5ba7cd](https://github.com/aws/aws-cdk/commit/b5ba7cdd61714bcfbf2135240790340a77ee1a8b)) +* **stepfunctions-tasks:** EcsRunTask now uses taskDefinition family instead of ARN ([#12436](https://github.com/aws/aws-cdk/issues/12436)) ([abde96b](https://github.com/aws/aws-cdk/commit/abde96b046358fc5435545692eba4fd63d503914)), closes [#12080](https://github.com/aws/aws-cdk/issues/12080) +* **stepfunctions-tasks:** support databrew startJobRun task ([#12532](https://github.com/aws/aws-cdk/issues/12532)) ([eacd2f7](https://github.com/aws/aws-cdk/commit/eacd2f7ea67c83d50c839acf29fbe953ae49d987)) + + +### Bug Fixes + +* **apigatewayv2:** multiple http integrations are created for each route ([#12528](https://github.com/aws/aws-cdk/issues/12528)) ([855ce59](https://github.com/aws/aws-cdk/commit/855ce59039a577d142d68720e86d81610edffc64)), closes [40aws-cdk/aws-apigatewayv2/lib/http/route.ts#L128](https://github.com/40aws-cdk/aws-apigatewayv2/lib/http/route.ts/issues/L128) +* **core:** modern deployments fail if bootstrap stack is renamed ([#12594](https://github.com/aws/aws-cdk/issues/12594)) ([e5c616f](https://github.com/aws/aws-cdk/commit/e5c616f73eac395492636341f57fb6a716d1ea69)), closes [#11952](https://github.com/aws/aws-cdk/issues/11952) [#11420](https://github.com/aws/aws-cdk/issues/11420) [#9053](https://github.com/aws/aws-cdk/issues/9053) +* **pipelines:** assets broken in Pipelines synthesized from Windows ([#12573](https://github.com/aws/aws-cdk/issues/12573)) ([5c3dce5](https://github.com/aws/aws-cdk/commit/5c3dce56c71083321069a31213aaa5bce40f51d3)), closes [#12540](https://github.com/aws/aws-cdk/issues/12540) +* **pipelines:** can't use CodePipeline variables in Synth environment variables ([#12602](https://github.com/aws/aws-cdk/issues/12602)) ([736b260](https://github.com/aws/aws-cdk/commit/736b260db7f21d89e220591007580f62b22fea3a)), closes [#12061](https://github.com/aws/aws-cdk/issues/12061) [#11178](https://github.com/aws/aws-cdk/issues/11178) +* **pipelines:** unable to publish assets inside VPC ([#12331](https://github.com/aws/aws-cdk/issues/12331)) ([a16f09c](https://github.com/aws/aws-cdk/commit/a16f09c9ea675caf5b1e50a4e1cc288e5afd1237)), closes [#11815](https://github.com/aws/aws-cdk/issues/11815) +* **s3-deployment:** User metadata keys have redundant triple `x-amz` prefix ([#12414](https://github.com/aws/aws-cdk/issues/12414)) ([6716181](https://github.com/aws/aws-cdk/commit/671618152dc585ef0703f6c3501f6ee5a366b4a9)), closes [#8459](https://github.com/aws/aws-cdk/issues/8459) +* **secretsmanager:** fromSecretPartialArn() has incorrect grant policies ([#12665](https://github.com/aws/aws-cdk/issues/12665)) ([560915e](https://github.com/aws/aws-cdk/commit/560915ece87a919f499a64452b919a0b291394ee)), closes [#12411](https://github.com/aws/aws-cdk/issues/12411) + ## [1.86.0](https://github.com/aws/aws-cdk/compare/v1.85.0...v1.86.0) (2021-01-21) @@ -25,6 +121,12 @@ All notable changes to this project will be documented in this file. See [standa ## [1.85.0](https://github.com/aws/aws-cdk/compare/v1.84.0...v1.85.0) (2021-01-14) * **s3-deployment**: This version includes an important update, please upgrade to prevent deployment failure. This is in prepartion of Lambda deprecation of the request module in boto, more details are available in [AWS blog](https://aws.amazon.com/blogs/compute/upcoming-changes-to-the-python-sdk-in-aws-lambda/). Note, users of versions < `1.81.0` will not be impacted by this deprecation, but are still encouraged to upgrade to the latest version. +* **s3**: The `grantWrite()` and `grantReadWrite()` methods no longer add the `s3:PutObject*` permissions that included `s3:PutObjectAcl`, + which could be used to grant read/write object access to IAM principals in other accounts. + This change is gated behind the `@aws-cdk/aws-s3:grantWriteWithoutAcl` feature flag, + so make sure to set it to `true` in the `context` key of your `cdk.json` file when upgrading. + If you still need the principal to have `s3:PutObjectAcl` permissions after upgrading, + use the new `grantPutAcl()` method. ### Features diff --git a/package.json b/package.json index 3cc060d034c26..31ec022ab52d6 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,13 @@ "devDependencies": { "conventional-changelog-cli": "^2.1.1", "fs-extra": "^9.1.0", - "graceful-fs": "^4.2.4", + "graceful-fs": "^4.2.6", "jest-junit": "^12.0.0", - "jsii-diff": "^1.15.0", - "jsii-pacmak": "^1.15.0", - "jsii-rosetta": "^1.15.0", + "jsii-diff": "^1.20.1", + "jsii-pacmak": "^1.20.1", + "jsii-rosetta": "^1.20.1", "lerna": "^3.22.1", - "standard-version": "^9.0.0", + "standard-version": "^9.1.0", "typescript": "~3.9.7" }, "resolutions-comment": "should be removed or reviewed when nodeunit dependency is dropped or adjusted", 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 fb93375423798..35436d11ce691 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 @@ -316,8 +316,7 @@ export class AppMeshExtension extends ServiceExtension { // Now create a virtual service. Relationship goes like this: // virtual service -> virtual router -> virtual node this.virtualService = new appmesh.VirtualService(this.scope, `${this.parentService.id}-virtual-service`, { - mesh: this.mesh, - virtualRouter: this.virtualRouter, + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualRouter(this.virtualRouter), virtualServiceName: serviceName, }); } diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index b122d9803fc8b..82eea1a46a6bb 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -63,7 +63,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "fast-check": "^2.11.0", + "fast-check": "^2.12.1", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts index 2f3352bee16ef..5a977d8252fd4 100644 --- a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts +++ b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts @@ -59,14 +59,14 @@ export class HaveResourceAssertion extends JestFriendlyAssertion properties === undefined ? anything() : allowValueExtension ? deepObjectLike(properties) : objectLike(properties); - this.part = part !== undefined ? part : ResourcePart.Properties; + this.part = part ?? ResourcePart.Properties; } public assertUsing(inspector: StackInspector): boolean { for (const logicalId of Object.keys(inspector.value.Resources || {})) { const resource = inspector.value.Resources[logicalId]; if (resource.Type === this.resourceType) { - const propsToCheck = this.part === ResourcePart.Properties ? resource.Properties : resource; + const propsToCheck = this.part === ResourcePart.Properties ? (resource.Properties ?? {}) : resource; // Pass inspection object as 2nd argument, initialize failure with default string, // to maintain backwards compatibility with old predicate API. diff --git a/packages/@aws-cdk/assert/package.json b/packages/@aws-cdk/assert/package.json index 9842fc4dcdfe3..1ebf15f57c87e 100644 --- a/packages/@aws-cdk/assert/package.json +++ b/packages/@aws-cdk/assert/package.json @@ -21,11 +21,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.4.4" + "ts-jest": "^26.5.1" }, "dependencies": { "@aws-cdk/cloud-assembly-schema": "0.0.0", diff --git a/packages/@aws-cdk/assets/lib/staging.ts b/packages/@aws-cdk/assets/lib/staging.ts index 3a441a0219f55..fe573ce442fdc 100644 --- a/packages/@aws-cdk/assets/lib/staging.ts +++ b/packages/@aws-cdk/assets/lib/staging.ts @@ -1,7 +1,11 @@ -import { AssetStaging, Construct } from '@aws-cdk/core'; +import { AssetStaging } from '@aws-cdk/core'; import { toSymlinkFollow } from './compat'; import { FingerprintOptions } from './fs/options'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Deprecated * @deprecated use `core.AssetStagingProps` diff --git a/packages/@aws-cdk/assets/package.json b/packages/@aws-cdk/assets/package.json index b4cbb05123930..cd15c2c2d83a5 100644 --- a/packages/@aws-cdk/assets/package.json +++ b/packages/@aws-cdk/assets/package.json @@ -69,14 +69,14 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "@types/sinon": "^9.0.9", + "@types/sinon": "^9.0.10", "aws-cdk": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0", - "sinon": "^9.2.1", - "ts-mock-imports": "^1.3.1" + "sinon": "^9.2.4", + "ts-mock-imports": "^1.3.3" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-amplify/lib/app.ts b/packages/@aws-cdk/aws-amplify/lib/app.ts index 8e7217a59cc58..bf1d5bc3d5e89 100644 --- a/packages/@aws-cdk/aws-amplify/lib/app.ts +++ b/packages/@aws-cdk/aws-amplify/lib/app.ts @@ -218,9 +218,9 @@ export class App extends Resource implements IApp, iam.IGrantable { basicAuthConfig: props.autoBranchCreation.basicAuth && props.autoBranchCreation.basicAuth.bind(this, 'BranchBasicAuth'), buildSpec: props.autoBranchCreation.buildSpec && props.autoBranchCreation.buildSpec.toBuildSpec(), enableAutoBranchCreation: true, - enableAutoBuild: props.autoBranchCreation.autoBuild === undefined ? true : props.autoBranchCreation.autoBuild, + enableAutoBuild: props.autoBranchCreation.autoBuild ?? true, environmentVariables: Lazy.any({ produce: () => renderEnvironmentVariables(this.autoBranchEnvironmentVariables ) }, { omitEmptyArray: true }), // eslint-disable-line max-len - enablePullRequestPreview: props.autoBranchCreation.pullRequestPreview === undefined ? true : props.autoBranchCreation.pullRequestPreview, + enablePullRequestPreview: props.autoBranchCreation.pullRequestPreview ?? true, pullRequestEnvironmentName: props.autoBranchCreation.pullRequestEnvironmentName, stage: props.autoBranchCreation.stage, }, diff --git a/packages/@aws-cdk/aws-amplify/lib/branch.ts b/packages/@aws-cdk/aws-amplify/lib/branch.ts index e26fe6c4d46a2..210f27d4831e2 100644 --- a/packages/@aws-cdk/aws-amplify/lib/branch.ts +++ b/packages/@aws-cdk/aws-amplify/lib/branch.ts @@ -139,8 +139,8 @@ export class Branch extends Resource implements IBranch { branchName, buildSpec: props.buildSpec && props.buildSpec.toBuildSpec(), description: props.description, - enableAutoBuild: props.autoBuild === undefined ? true : props.autoBuild, - enablePullRequestPreview: props.pullRequestPreview === undefined ? true : props.pullRequestPreview, + enableAutoBuild: props.autoBuild ?? true, + enablePullRequestPreview: props.pullRequestPreview ?? true, environmentVariables: Lazy.any({ produce: () => renderEnvironmentVariables(this.environmentVariables) }, { omitEmptyArray: true }), pullRequestEnvironmentName: props.pullRequestEnvironmentName, stage: props.stage, diff --git a/packages/@aws-cdk/aws-amplify/lib/domain.ts b/packages/@aws-cdk/aws-amplify/lib/domain.ts index b657ebdefb247..bf6ba7afe8017 100644 --- a/packages/@aws-cdk/aws-amplify/lib/domain.ts +++ b/packages/@aws-cdk/aws-amplify/lib/domain.ts @@ -147,7 +147,7 @@ export class Domain extends Resource { private renderSubDomainSettings() { return this.subDomains.map(s => ({ branchName: s.branch.branchName, - prefix: s.prefix === undefined ? s.branch.branchName : s.prefix, + prefix: s.prefix ?? s.branch.branchName, })); } } diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 5e670032ef158..4d4d1a79eb797 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -32,6 +32,7 @@ running on AWS Lambda, or any web application. - [IAM-based authorizer](#iam-based-authorizer) - [Lambda-based token authorizer](#lambda-based-token-authorizer) - [Lambda-based request authorizer](#lambda-based-request-authorizer) + - [Cognito User Pools authorizer](#cognito-user-pools-authorizer) - [Mutual TLS](#mutal-tls-mtls) - [Deployments](#deployments) - [Deep dive: Invalidation of deployments](#deep-dive-invalidation-of-deployments) @@ -580,6 +581,25 @@ Authorizers can also be passed via the `defaultMethodOptions` property within th explicitly overridden, the specified defaults will be applied across all `Method`s across the `RestApi` or across all `Resource`s, depending on where the defaults were specified. +### Cognito User Pools authorizer + +API Gateway also allows [Amazon Cognito user pools as authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html) + +The following snippet configures a Cognito user pool as an authorizer: + +```ts +const userPool = new cognito.UserPool(stack, 'UserPool'); + +const auth = new apigateway.CognitoUserPoolsAuthorizer(this, 'booksAuthorizer', { + cognitoUserPools: [userPool] +}); + +books.addMethod('GET', new apigateway.HttpIntegration('http://amazon.com'), { + authorizer: auth, + authorizationType: apigateway.AuthorizationType.COGNITO, +}); +``` + ## Mutual TLS (mTLS) Mutual TLS can be configured to limit access to your API based by using client certificates instead of (or as an extension of) using authorization headers. @@ -809,9 +829,10 @@ new apigateway.RestApi(this, 'books', { deployOptions: { accessLogDestination: new apigateway.LogGroupLogDestination(logGroup), accessLogFormat: apigateway.AccessLogFormat.custom( - `${AccessLogFormat.contextRequestId()} ${AccessLogField.contextErrorMessage()} ${AccessLogField.contextErrorMessageString()}`); - }) -}; + `${AccessLogField.contextRequestId()} ${AccessLogField.contextErrorMessage()} ${AccessLogField.contextErrorMessageString()}` + ) + } +}); ``` You can use the `methodOptions` property to configure diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/cognito.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/cognito.ts new file mode 100644 index 0000000000000..a1d000189354c --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/cognito.ts @@ -0,0 +1,115 @@ +import * as cognito from '@aws-cdk/aws-cognito'; +import { Duration, Lazy, Names, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnAuthorizer } from '../apigateway.generated'; +import { Authorizer, IAuthorizer } from '../authorizer'; +import { AuthorizationType } from '../method'; +import { IRestApi } from '../restapi'; + +/** + * Properties for CognitoUserPoolsAuthorizer + */ +export interface CognitoUserPoolsAuthorizerProps { + /** + * An optional human friendly name for the authorizer. Note that, this is not the primary identifier of the authorizer. + * + * @default - the unique construct ID + */ + readonly authorizerName?: string; + + /** + * The user pools to associate with this authorizer. + */ + readonly cognitoUserPools: cognito.IUserPool[]; + + /** + * How long APIGateway should cache the results. Max 1 hour. + * Disable caching by setting this to 0. + * + * @default Duration.minutes(5) + */ + readonly resultsCacheTtl?: Duration; + + /** + * The request header mapping expression for the bearer token. This is typically passed as part of the header, in which case + * this should be `method.request.header.Authorizer` where Authorizer is the header containing the bearer token. + * @see https://docs.aws.amazon.com/apigateway/api-reference/link-relation/authorizer-create/#identitySource + * @default `IdentitySource.header('Authorization')` + */ + readonly identitySource?: string; +} + +/** + * Cognito user pools based custom authorizer + * + * @resource AWS::ApiGateway::Authorizer + */ +export class CognitoUserPoolsAuthorizer extends Authorizer implements IAuthorizer { + /** + * The id of the authorizer. + * @attribute + */ + public readonly authorizerId: string; + + /** + * The ARN of the authorizer to be used in permission policies, such as IAM and resource-based grants. + * @attribute + */ + public readonly authorizerArn: string; + + /** + * The authorization type of this authorizer. + */ + public readonly authorizationType?: AuthorizationType; + + private restApiId?: string; + + constructor(scope: Construct, id: string, props: CognitoUserPoolsAuthorizerProps) { + super(scope, id); + + const restApiId = this.lazyRestApiId(); + const resource = new CfnAuthorizer(this, 'Resource', { + name: props.authorizerName ?? Names.uniqueId(this), + restApiId, + type: 'COGNITO_USER_POOLS', + providerArns: props.cognitoUserPools.map(userPool => userPool.userPoolArn), + authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds(), + identitySource: props.identitySource || 'method.request.header.Authorization', + }); + + this.authorizerId = resource.ref; + this.authorizerArn = Stack.of(this).formatArn({ + service: 'execute-api', + resource: restApiId, + resourceName: `authorizers/${this.authorizerId}`, + }); + this.authorizationType = AuthorizationType.COGNITO; + } + + /** + * Attaches this authorizer to a specific REST API. + * @internal + */ + public _attachToApi(restApi: IRestApi): void { + if (this.restApiId && this.restApiId !== restApi.restApiId) { + throw new Error('Cannot attach authorizer to two different rest APIs'); + } + + this.restApiId = restApi.restApiId; + } + + /** + * Returns a token that resolves to the Rest Api Id at the time of synthesis. + * Throws an error, during token resolution, if no RestApi is attached to this authorizer. + */ + private lazyRestApiId() { + return Lazy.string({ + produce: () => { + if (!this.restApiId) { + throw new Error(`Authorizer (${this.node.path}) must be attached to a RestApi`); + } + return this.restApiId; + }, + }); + } +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/index.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/index.ts index 57289c931f760..fd93db036fefe 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/index.ts @@ -1,2 +1,3 @@ export * from './lambda'; export * from './identity-source'; +export * from './cognito'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index a286e978fdafb..d44b2ab223f4a 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -1,10 +1,14 @@ import * as crypto from 'crypto'; -import { Construct as CoreConstruct, Lazy, RemovalPolicy, Resource, CfnResource } from '@aws-cdk/core'; +import { Lazy, RemovalPolicy, Resource, CfnResource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnDeployment } from './apigateway.generated'; import { Method } from './method'; import { IRestApi, RestApi, SpecRestApi, RestApiBase } from './restapi'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + export interface DeploymentProps { /** * The Rest API to deploy. diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/http.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/http.ts index fac5bb7cc6800..65f0657e19817 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/http.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/http.ts @@ -38,7 +38,7 @@ export interface HttpIntegrationProps { */ export class HttpIntegration extends Integration { constructor(url: string, props: HttpIntegrationProps = { }) { - const proxy = props.proxy !== undefined ? props.proxy : true; + const proxy = props.proxy ?? true; const method = props.httpMethod || 'GET'; super({ type: proxy ? IntegrationType.HTTP_PROXY : IntegrationType.HTTP, diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts index 399484510f343..97772fe7eddb5 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts @@ -41,7 +41,7 @@ export class LambdaIntegration extends AwsIntegration { private readonly enableTest: boolean; constructor(handler: lambda.IFunction, options: LambdaIntegrationOptions = { }) { - const proxy = options.proxy === undefined ? true : options.proxy; + const proxy = options.proxy ?? true; super({ proxy, @@ -51,7 +51,7 @@ export class LambdaIntegration extends AwsIntegration { }); this.handler = handler; - this.enableTest = options.allowTestInvoke === undefined ? true : options.allowTestInvoke; + this.enableTest = options.allowTestInvoke ?? true; } public bind(method: Method): IntegrationConfig { diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index 04d9598303ad8..49d0bf0356d80 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -276,7 +276,7 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc // // statusCode - const statusCode = options.statusCode !== undefined ? options.statusCode : 204; + const statusCode = options.statusCode ?? 204; // // prepare responseParams @@ -373,7 +373,51 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc } } +/** + * Attributes that can be specified when importing a Resource + */ +export interface ResourceAttributes { + /** + * The ID of the resource. + */ + readonly resourceId: string; + + /** + * The rest API that this resource is part of. + */ + readonly restApi: IRestApi; + + /** + * The full path of this resource. + */ + readonly path: string; +} + export class Resource extends ResourceBase { + /** + * Import an existing resource + */ + public static fromResourceAttributes(scope: Construct, id: string, attrs: ResourceAttributes): IResource { + class Import extends ResourceBase { + public readonly api = attrs.restApi; + public readonly resourceId = attrs.resourceId; + public readonly path = attrs.path; + public readonly defaultIntegration?: Integration = undefined; + public readonly defaultMethodOptions?: MethodOptions = undefined; + public readonly defaultCorsPreflightOptions?: CorsOptions = undefined; + + public get parentResource(): IResource { + throw new Error('parentResource is not configured for imported resource.'); + } + + public get restApi(): RestApi { + throw new Error('restApi is not configured for imported resource.'); + } + } + + return new Import(scope, id); + } + public readonly parentResource?: IResource; public readonly api: IRestApi; public readonly resourceId: string; @@ -478,7 +522,7 @@ export class ProxyResource extends Resource { defaultMethodOptions: props.defaultMethodOptions, }); - const anyMethod = props.anyMethod !== undefined ? props.anyMethod : true; + const anyMethod = props.anyMethod ?? true; if (anyMethod) { this.anyMethod = this.addMethod('ANY'); } diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 0911c92dda94b..32c400c52d23e 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -504,7 +504,7 @@ export abstract class RestApiBase extends Resource implements IRestApi { } protected configureDeployment(props: RestApiOptions) { - const deploy = props.deploy === undefined ? true : props.deploy; + const deploy = props.deploy ?? true; if (deploy) { this._latestDeployment = new Deployment(this, 'Deployment', { @@ -593,7 +593,7 @@ export class SpecRestApi extends RestApiBase { name: this.restApiName, policy: props.policy, failOnWarnings: props.failOnWarnings, - body: apiDefConfig.inlineDefinition ? apiDefConfig.inlineDefinition : undefined, + body: apiDefConfig.inlineDefinition ?? undefined, bodyS3Location: apiDefConfig.inlineDefinition ? undefined : apiDefConfig.s3Location, endpointConfiguration: this._configureEndpoints(props), parameters: props.parameters, @@ -608,7 +608,7 @@ export class SpecRestApi extends RestApiBase { this.addDomainName('CustomDomain', props.domainName); } - const cloudWatchRole = props.cloudWatchRole !== undefined ? props.cloudWatchRole : true; + const cloudWatchRole = props.cloudWatchRole ?? true; if (cloudWatchRole) { this.configureCloudWatchRole(resource); } @@ -700,13 +700,13 @@ export class RestApi extends RestApiBase { binaryMediaTypes: props.binaryMediaTypes, endpointConfiguration: this._configureEndpoints(props), apiKeySourceType: props.apiKeySourceType, - cloneFrom: props.cloneFrom ? props.cloneFrom.restApiId : undefined, + cloneFrom: props.cloneFrom?.restApiId, parameters: props.parameters, }); this.node.defaultChild = resource; this.restApiId = resource.ref; - const cloudWatchRole = props.cloudWatchRole !== undefined ? props.cloudWatchRole : true; + const cloudWatchRole = props.cloudWatchRole ?? true; if (cloudWatchRole) { this.configureCloudWatchRole(resource); } diff --git a/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts b/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts index 6a1c5a5091bda..ad807d4a7d2d0 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts @@ -179,8 +179,10 @@ export class UsagePlan extends Resource { * @param apiKey */ public addApiKey(apiKey: IApiKey): void { + const prefix = 'UsagePlanKeyResource'; + // Postfixing apikey id only from the 2nd child, to keep physicalIds of UsagePlanKey for existing CDK apps unmodifed. - const id = `UsagePlanKeyResource:${Names.nodeUniqueId(apiKey.node)}`; + 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/package.json b/packages/@aws-cdk/aws-apigateway/package.json index f8f1296629c0a..3a0641e8b1162 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -78,9 +78,9 @@ "pkglint": "0.0.0" }, "dependencies": { - "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", @@ -94,9 +94,9 @@ }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { - "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts new file mode 100644 index 0000000000000..e59339177d5d4 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/cognito.test.ts @@ -0,0 +1,66 @@ +import '@aws-cdk/assert/jest'; +import * as cognito from '@aws-cdk/aws-cognito'; +import { Duration, Stack } from '@aws-cdk/core'; +import { AuthorizationType, CognitoUserPoolsAuthorizer, RestApi } from '../../lib'; + +describe('Cognito Authorizer', () => { + test('default cognito authorizer', () => { + // GIVEN + const stack = new Stack(); + const userPool = new cognito.UserPool(stack, 'UserPool'); + + // WHEN + const authorizer = new CognitoUserPoolsAuthorizer(stack, 'myauthorizer', { + cognitoUserPools: [userPool], + }); + + const restApi = new RestApi(stack, 'myrestapi'); + restApi.root.addMethod('ANY', undefined, { + authorizer, + authorizationType: AuthorizationType.COGNITO, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Type: 'COGNITO_USER_POOLS', + RestApiId: stack.resolve(restApi.restApiId), + IdentitySource: 'method.request.header.Authorization', + ProviderARNs: [stack.resolve(userPool.userPoolArn)], + }); + + expect(authorizer.authorizerArn.endsWith(`/authorizers/${authorizer.authorizerId}`)).toBeTruthy(); + }); + + test('cognito authorizer with all parameters specified', () => { + // GIVEN + const stack = new Stack(); + const userPool1 = new cognito.UserPool(stack, 'UserPool1'); + const userPool2 = new cognito.UserPool(stack, 'UserPool2'); + + // WHEN + const authorizer = new CognitoUserPoolsAuthorizer(stack, 'myauthorizer', { + cognitoUserPools: [userPool1, userPool2], + identitySource: 'method.request.header.whoami', + authorizerName: 'myauthorizer', + resultsCacheTtl: Duration.minutes(1), + }); + + const restApi = new RestApi(stack, 'myrestapi'); + restApi.root.addMethod('ANY', undefined, { + authorizer, + authorizationType: AuthorizationType.COGNITO, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGateway::Authorizer', { + Type: 'COGNITO_USER_POOLS', + Name: 'myauthorizer', + RestApiId: stack.resolve(restApi.restApiId), + IdentitySource: 'method.request.header.whoami', + AuthorizerResultTtlInSeconds: 60, + ProviderARNs: [stack.resolve(userPool1.userPoolArn), stack.resolve(userPool2.userPoolArn)], + }); + + expect(authorizer.authorizerArn.endsWith(`/authorizers/${authorizer.authorizerId}`)).toBeTruthy(); + }); +}); diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.expected.json new file mode 100644 index 0000000000000..990619cb495d4 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.expected.json @@ -0,0 +1,191 @@ +{ + "Resources": { + "UserPool6BA7E5F2": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + } + }, + "myauthorizer23CB99DD": { + "Type": "AWS::ApiGateway::Authorizer", + "Properties": { + "RestApiId": { + "Ref": "myrestapi551C8392" + }, + "Type": "COGNITO_USER_POOLS", + "IdentitySource": "method.request.header.Authorization", + "Name": "CognitoUserPoolsAuthorizerIntegmyauthorizer10C804C1", + "ProviderARNs": [ + { + "Fn::GetAtt": [ + "UserPool6BA7E5F2", + "Arn" + ] + } + ] + } + }, + "myrestapi551C8392": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "myrestapi" + } + }, + "myrestapiCloudWatchRoleC48DA1DD": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "myrestapiAccountA49A05BE": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "myrestapiCloudWatchRoleC48DA1DD", + "Arn" + ] + } + }, + "DependsOn": [ + "myrestapi551C8392" + ] + }, + "myrestapiDeployment419B1464b903292b53d7532ca4296973bcb95b1a": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myrestapi551C8392" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "myrestapiANY94B0497F" + ] + }, + "myrestapiDeploymentStageprodA9250EA4": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "myrestapi551C8392" + }, + "DeploymentId": { + "Ref": "myrestapiDeployment419B1464b903292b53d7532ca4296973bcb95b1a" + }, + "StageName": "prod" + } + }, + "myrestapiANY94B0497F": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "myrestapi551C8392", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "myrestapi551C8392" + }, + "AuthorizationType": "COGNITO_USER_POOLS", + "AuthorizerId": { + "Ref": "myauthorizer23CB99DD" + }, + "Integration": { + "IntegrationResponses": [ + { + "StatusCode": "200" + } + ], + "PassthroughBehavior": "NEVER", + "RequestTemplates": { + "application/json": "{ \"statusCode\": 200 }" + }, + "Type": "MOCK" + }, + "MethodResponses": [ + { + "StatusCode": "200" + } + ] + } + } + }, + "Outputs": { + "myrestapiEndpointE06F9D98": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myrestapi551C8392" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myrestapiDeploymentStageprodA9250EA4" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.ts new file mode 100644 index 0000000000000..4830dc83ae29f --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.cognito-authorizer.ts @@ -0,0 +1,43 @@ +import * as cognito from '@aws-cdk/aws-cognito'; +import { App, Stack } from '@aws-cdk/core'; +import { AuthorizationType, CognitoUserPoolsAuthorizer, MockIntegration, PassthroughBehavior, RestApi } from '../../lib'; + +/* + * Stack verification steps: + * * 1. Get the IdToken for the created pool by adding user/app-client and using aws cognito-idp: + * * a. aws cognito-idp create-user-pool-client --user-pool-id --client-name --no-generate-secret + * * b. aws cognito-idp admin-create-user --user-pool-id --username --temporary-password + * * c. aws cognito-idp initiate-auth --client-id --auth-flow USER_PASSWORD_AUTH --auth-parameters USERNAME=,PASSWORD= + * * d. aws cognito-idp respond-to-auth-challenge --client-id --challenge-name --session + * * + * * 2. Verify the stack using above obtained token: + * * a. `curl -s -o /dev/null -w "%{http_code}" ` should return 401 + * * b. `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: ' ` should return 403 + * * c. `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: ' ` should return 200 + */ + +const app = new App(); +const stack = new Stack(app, 'CognitoUserPoolsAuthorizerInteg'); + +const userPool = new cognito.UserPool(stack, 'UserPool'); + +const authorizer = new CognitoUserPoolsAuthorizer(stack, 'myauthorizer', { + cognitoUserPools: [userPool], +}); + +const restApi = new RestApi(stack, 'myrestapi'); +restApi.root.addMethod('ANY', new MockIntegration({ + integrationResponses: [ + { statusCode: '200' }, + ], + passthroughBehavior: PassthroughBehavior.NEVER, + requestTemplates: { + 'application/json': '{ "statusCode": 200 }', + }, +}), { + methodResponses: [ + { statusCode: '200' }, + ], + authorizer, + authorizationType: AuthorizationType.COGNITO, +}); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json index a0fb6357db3c7..91af3471593eb 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -602,7 +602,7 @@ "UsagePlanName": "Basic" } }, - "myapiUsagePlanUsagePlanKeyResourcetestapigatewayrestapimyapiApiKeyC43601CB600D112D": { + "myapiUsagePlanUsagePlanKeyResource050D133F": { "Type": "AWS::ApiGateway::UsagePlanKey", "Properties": { "KeyId": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json index 9dee2e7aa07b0..8e761f40e2a26 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json @@ -3,7 +3,7 @@ "myusageplan4B391740": { "Type": "AWS::ApiGateway::UsagePlan" }, - "myusageplanUsagePlanKeyResourcetestapigatewayusageplanmultikeymyapikey1DDABC389A2809A73": { + "myusageplanUsagePlanKeyResource095B4EA9": { "Type": "AWS::ApiGateway::UsagePlanKey", "Properties": { "KeyId": { diff --git a/packages/@aws-cdk/aws-apigateway/test/resource.test.ts b/packages/@aws-cdk/aws-apigateway/test/resource.test.ts index ecad61cb1905c..0a20483f4261c 100644 --- a/packages/@aws-cdk/aws-apigateway/test/resource.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/resource.test.ts @@ -236,6 +236,27 @@ describe('resource', () => { }); + test('fromResourceAttributes()', () => { + // GIVEN + const stack = new Stack(); + const resourceId = 'resource-id'; + const api = new apigw.RestApi(stack, 'MyRestApi'); + + // WHEN + const imported = apigw.Resource.fromResourceAttributes(stack, 'imported-resource', { + resourceId, + restApi: api, + path: 'some-path', + }); + imported.addMethod('GET'); + + // THEN + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + HttpMethod: 'GET', + ResourceId: resourceId, + }); + }); + describe('getResource', () => { describe('root resource', () => { diff --git a/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts b/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts index 854c0a65a6562..f183d08796388 100644 --- a/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts @@ -205,32 +205,4 @@ describe('usage plan', () => { }, }, ResourcePart.Properties); }); - - test('UsagePlanKeys have unique logical ids', () => { - // GIVEN - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'my-stack'); - const usagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan'); - const apiKey1 = new apigateway.ApiKey(stack, 'my-api-key-1', { - apiKeyName: 'my-api-key-1', - }); - const apiKey2 = new apigateway.ApiKey(stack, 'my-api-key-2', { - apiKeyName: 'my-api-key-2', - }); - - // WHEN - usagePlan.addApiKey(apiKey1); - usagePlan.addApiKey(apiKey2); - - // THEN - const template = app.synth().getStackByName(stack.stackName).template; - const logicalIds = Object.entries(template.Resources) - .filter(([_, v]) => (v as any).Type === 'AWS::ApiGateway::UsagePlanKey') - .map(([k, _]) => k); - - expect(logicalIds).toEqual([ - 'myusageplanUsagePlanKeyResourcemystackmyapikey1EE9AA1B359121274', - 'myusageplanUsagePlanKeyResourcemystackmyapikey2B4E8EB1456DC88E9', - ]); - }); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/.eslintrc.js b/packages/@aws-cdk/aws-apigatewayv2-authorizers/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/.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-authorizers/.gitignore b/packages/@aws-cdk/aws-apigatewayv2-authorizers/.gitignore new file mode 100644 index 0000000000000..becda34c45624 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/.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-authorizers/.npmignore b/packages/@aws-cdk/aws-apigatewayv2-authorizers/.npmignore new file mode 100644 index 0000000000000..093c734b1bd2f --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/.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-authorizers/LICENSE b/packages/@aws-cdk/aws-apigatewayv2-authorizers/LICENSE new file mode 100644 index 0000000000000..28e4bdcec77ec --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/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-2021 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-authorizers/NOTICE b/packages/@aws-cdk/aws-apigatewayv2-authorizers/NOTICE new file mode 100644 index 0000000000000..5fc3826926b5b --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/README.md b/packages/@aws-cdk/aws-apigatewayv2-authorizers/README.md new file mode 100644 index 0000000000000..9591ecfe4ffde --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/README.md @@ -0,0 +1,82 @@ +# AWS APIGatewayv2 Authorizers + + +--- + +![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 + +- [HTTP APIs](#http-apis) +- [JWT Authorizers](#jwt-authorizers) + - [User Pool Authorizer](#user-pool-authorizer) + +## HTTP APIs + +API Gateway supports multiple mechanisms for controlling and managing access to your HTTP API. They are mainly +classified into Lambda Authorizers, JWT authorizers and standard AWS IAM roles and policies. More information is +available at [Controlling and managing access to an HTTP +API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-access-control.html). + +## JWT Authorizers + +JWT authorizers allow the use of JSON Web Tokens (JWTs) as part of [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) and [OAuth 2.0](https://oauth.net/2/) frameworks to allow and restrict clients from accessing HTTP APIs. + +When configured on a route, the API Gateway service validates the JWTs submitted by the client, and allows or denies access based on its content. + +API gateway uses the `identitySource` to determine where to look for the token. By default it checks the http `Authorization` header. However it also [supports a number of other options](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html#http-api-lambda-authorizer.identity-sources). It then decodes the JWT and validates the signature and claims, against the options defined in the authorizer and route (scopes). For more information check the [JWT Authorizer documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html). + +```ts +const authorizer = new HttpJwtAuthorizer({ + jwtAudience: ['3131231'], + jwtIssuer: 'https://test.us.auth0.com', +}); + +const api = new HttpApi(stack, 'HttpApi'); + +api.addRoutes({ + integration: new HttpProxyIntegration({ + url: 'https://get-books-proxy.myproxy.internal', + }), + path: '/books', + authorizer, +}); +``` + +### User Pool Authorizer + +User Pool Authorizer is a type of JWT Authorizer that uses a Cognito user pool and app client to control who can access your Api. After a successful authorization from the app client, the generated access token will be used as the JWT. + +Clients accessing an API that uses a user pool authorizer must first sign in to a user pool and obtain an identity or access token. +They must then use this token in the `Authorization` header of the API call. More information is available at [using Amazon Cognito user +pools as authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html). + +```ts +const userPool = new UserPool(stack, 'UserPool'); +const userPoolClient = userPool.addClient('UserPoolClient'); + +const authorizer = new HttpUserPoolAuthorizer({ + userPool, + userPoolClient, +}); + +const api = new HttpApi(stack, 'HttpApi'); + +api.addRoutes({ + integration: new HttpProxyIntegration({ + url: 'https://get-books-proxy.myproxy.internal', + }), + path: '/books', + authorizer, +}); +``` diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/jest.config.js b/packages/@aws-cdk/aws-apigatewayv2-authorizers/jest.config.js new file mode 100644 index 0000000000000..b5ccdecc15ee0 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + ...baseConfig.coverageThreshold.global, + branches: 70, + }, + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/index.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/index.ts new file mode 100644 index 0000000000000..9f9ad94c6a4b7 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/index.ts @@ -0,0 +1,2 @@ +export * from './user-pool'; +export * from './jwt'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/jwt.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/jwt.ts new file mode 100644 index 0000000000000..afb5f10ac07f8 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/jwt.ts @@ -0,0 +1,70 @@ +import { + HttpAuthorizer, + HttpAuthorizerType, + HttpRouteAuthorizerBindOptions, + HttpRouteAuthorizerConfig, + IHttpRouteAuthorizer, +} from '@aws-cdk/aws-apigatewayv2'; +import { Token } from '@aws-cdk/core'; + +/** + * Properties to initialize HttpJwtAuthorizer. + */ +export interface HttpJwtAuthorizerProps { + + /** + * The name of the authorizer + * @default 'JwtAuthorizer' + */ + readonly authorizerName?: string; + + /** + * The identity source for which authorization is requested. + * + * @default ['$request.header.Authorization'] + */ + readonly identitySource?: string[], + + /** + * A list of the intended recipients of the JWT. + * A valid JWT must provide an aud that matches at least one entry in this list. + */ + readonly jwtAudience: string[] + + /** + * The base domain of the identity provider that issues JWT. + */ + readonly jwtIssuer: string; +} + +/** + * Authorize Http Api routes on whether the requester is registered as part of + * an AWS Cognito user pool. + */ +export class HttpJwtAuthorizer implements IHttpRouteAuthorizer { + private authorizer?: HttpAuthorizer; + + constructor(private readonly props: HttpJwtAuthorizerProps) { + } + + public bind(options: HttpRouteAuthorizerBindOptions): HttpRouteAuthorizerConfig { + if (!this.authorizer) { + const id = this.props.authorizerName && !Token.isUnresolved(this.props.authorizerName) ? + this.props.authorizerName : 'JwtAuthorizer'; + + this.authorizer = new HttpAuthorizer(options.scope, id, { + httpApi: options.route.httpApi, + identitySource: this.props.identitySource ?? ['$request.header.Authorization'], + type: HttpAuthorizerType.JWT, + authorizerName: this.props.authorizerName, + jwtAudience: this.props.jwtAudience, + jwtIssuer: this.props.jwtIssuer, + }); + } + + return { + authorizerId: this.authorizer.authorizerId, + authorizationType: HttpAuthorizerType.JWT, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/user-pool.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/user-pool.ts new file mode 100644 index 0000000000000..4a251b8eb7406 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/http/user-pool.ts @@ -0,0 +1,69 @@ +import { HttpAuthorizer, HttpAuthorizerType, HttpRouteAuthorizerBindOptions, HttpRouteAuthorizerConfig, IHttpRouteAuthorizer } from '@aws-cdk/aws-apigatewayv2'; +import { IUserPool, IUserPoolClient } from '@aws-cdk/aws-cognito'; +import { Stack, Token } from '@aws-cdk/core'; + +/** + * Properties to initialize UserPoolAuthorizer. + */ +export interface UserPoolAuthorizerProps { + /** + * The user pool client that should be used to authorize requests with the user pool. + */ + readonly userPoolClient: IUserPoolClient; + + /** + * The associated user pool + */ + readonly userPool: IUserPool; + + /** + * The AWS region in which the user pool is present + * @default - same region as the Route the authorizer is attached to. + */ + readonly userPoolRegion?: string; + + /** + * The name of the authorizer + * @default 'UserPoolAuthorizer' + */ + readonly authorizerName?: string; + + /** + * The identity source for which authorization is requested. + * + * @default ['$request.header.Authorization'] + */ + readonly identitySource?: string[], +} + +/** + * Authorize Http Api routes on whether the requester is registered as part of + * an AWS Cognito user pool. + */ +export class HttpUserPoolAuthorizer implements IHttpRouteAuthorizer { + private authorizer?: HttpAuthorizer; + + constructor(private readonly props: UserPoolAuthorizerProps) { + } + + public bind(options: HttpRouteAuthorizerBindOptions): HttpRouteAuthorizerConfig { + if (!this.authorizer) { + const id = this.props.authorizerName && !Token.isUnresolved(this.props.authorizerName) ? + this.props.authorizerName : 'UserPoolAuthorizer'; + const region = this.props.userPoolRegion ?? Stack.of(options.scope).region; + this.authorizer = new HttpAuthorizer(options.scope, id, { + httpApi: options.route.httpApi, + identitySource: this.props.identitySource ?? ['$request.header.Authorization'], + type: HttpAuthorizerType.JWT, + authorizerName: this.props.authorizerName, + jwtAudience: [this.props.userPoolClient.userPoolClientId], + jwtIssuer: `https://cognito-idp.${region}.amazonaws.com/${this.props.userPool.userPoolId}`, + }); + } + + return { + authorizerId: this.authorizer.authorizerId, + authorizationType: HttpAuthorizerType.JWT, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/index.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/index.ts new file mode 100644 index 0000000000000..c202386ae710e --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/lib/index.ts @@ -0,0 +1 @@ +export * from './http'; diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json b/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json new file mode 100644 index 0000000000000..de6dffe1edc61 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/package.json @@ -0,0 +1,101 @@ +{ + "name": "@aws-cdk/aws-apigatewayv2-authorizers", + "version": "0.0.0", + "description": "Authorizers for AWS APIGateway V2", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.APIGatewayv2.Authorizers", + "packageId": "Amazon.CDK.AWS.APIGatewayv2.Authorizers", + "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.authorizers", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "apigatewayv2-authorizers" + } + }, + "python": { + "distName": "aws-cdk.aws-apigatewayv2-authorizers", + "module": "aws_cdk.aws_apigatewayv2_authorizers", + "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-authorizers" + }, + "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": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "rosetta:extract": "yarn --silent jsii-rosetta extract" + }, + "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", + "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", + "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" + }, + "peerDependencies": { + "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-cognito": "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-authorizers/test/http/integ.user-pool.expected.json b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.expected.json new file mode 100644 index 0000000000000..cdfdf727a21f3 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.expected.json @@ -0,0 +1,288 @@ +{ + "Resources": { + "MyHttpApi8AEAAC21": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "MyHttpApi", + "ProtocolType": "HTTP" + } + }, + "MyHttpApiDefaultStageDCB9BC49": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "StageName": "$default", + "AutoDeploy": true + } + }, + "MyHttpApiGETAuthorizerIntegMyHttpApiGET16D02385PermissionBB02EBFE": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "lambda8B5974B5", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyHttpApi8AEAAC21" + }, + "/*/*/" + ] + ] + } + } + }, + "MyHttpApiGETE0EFC6F8": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "RouteKey": "GET /", + "AuthorizationScopes": [], + "AuthorizationType": "JWT", + "AuthorizerId": { + "Ref": "MyHttpApiUserPoolAuthorizer8754262B" + }, + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "MyHttpApiHttpIntegration6f095b8469365f72e33fa33d9711b140C45F3B26" + } + ] + ] + } + } + }, + "MyHttpApiHttpIntegration6f095b8469365f72e33fa33d9711b140C45F3B26": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "lambda8B5974B5", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + } + }, + "MyHttpApiUserPoolAuthorizer8754262B": { + "Type": "AWS::ApiGatewayV2::Authorizer", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "AuthorizerType": "JWT", + "IdentitySource": [ + "$request.header.Authorization" + ], + "Name": "UserPoolAuthorizer", + "JwtConfiguration": { + "Audience": [ + { + "Ref": "userpoolmyclientFAD947AB" + } + ], + "Issuer": { + "Fn::Join": [ + "", + [ + "https://cognito-idp.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com/", + { + "Ref": "userpool0AC4AA96" + } + ] + ] + } + } + } + }, + "userpool0AC4AA96": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + } + }, + "userpoolmyclientFAD947AB": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "userpool0AC4AA96" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + }, + "lambdaServiceRole494E4CA6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "lambda8B5974B5": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cdS3Bucket0AFE1748" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cdS3VersionKey8E654BCC" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cdS3VersionKey8E654BCC" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "lambdaServiceRole494E4CA6", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "lambdaServiceRole494E4CA6" + ] + } + }, + "Parameters": { + "AssetParameters7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cdS3Bucket0AFE1748": { + "Type": "String", + "Description": "S3 bucket for asset \"7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cd\"" + }, + "AssetParameters7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cdS3VersionKey8E654BCC": { + "Type": "String", + "Description": "S3 key for asset version \"7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cd\"" + }, + "AssetParameters7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cdArtifactHashC4761AE9": { + "Type": "String", + "Description": "Artifact hash for asset \"7410bbb25893071ddf955447cf906ac518465ea509469e6b012c28dde8f8b5cd\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.ts new file mode 100644 index 0000000000000..edf455f4a787c --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.ts @@ -0,0 +1,42 @@ +/// !cdk-integ pragma:ignore-assets +import * as path from 'path'; +import { HttpApi, HttpMethod } from '@aws-cdk/aws-apigatewayv2'; +import { LambdaProxyIntegration } from '@aws-cdk/aws-apigatewayv2-integrations'; +import * as cognito from '@aws-cdk/aws-cognito'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { App, Stack } from '@aws-cdk/core'; +import { HttpUserPoolAuthorizer } from '../../lib'; + +/* + * Stack verification steps: + * * `curl -s -o /dev/null -w "%{http_code}" ` should return 401 + * * `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: deny' ` should return 403 + * * `curl -s -o /dev/null -w "%{http_code}" -H 'Authorization: allow' ` should return 200 + */ + +const app = new App(); +const stack = new Stack(app, 'AuthorizerInteg'); + +const httpApi = new HttpApi(stack, 'MyHttpApi'); + +const userPool = new cognito.UserPool(stack, 'userpool'); + +const userPoolClient = userPool.addClient('my-client'); + +const authorizer = new HttpUserPoolAuthorizer({ + userPool, + userPoolClient, +}); + +const handler = new lambda.Function(stack, 'lambda', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.AssetCode.fromAsset(path.join(__dirname, '../integ.user-pool.handler')), +}); + +httpApi.addRoutes({ + path: '/', + methods: [HttpMethod.GET], + integration: new LambdaProxyIntegration({ handler }), + authorizer, +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/jwt.test.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/jwt.test.ts new file mode 100644 index 0000000000000..8b27dc312a1a3 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/jwt.test.ts @@ -0,0 +1,70 @@ +import '@aws-cdk/assert/jest'; +import { HttpApi, HttpIntegrationType, HttpRouteIntegrationBindOptions, IHttpRouteIntegration, PayloadFormatVersion } from '@aws-cdk/aws-apigatewayv2'; +import { Stack } from '@aws-cdk/core'; +import { HttpJwtAuthorizer } from '../../lib'; + +describe('HttpJwtAuthorizer', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + + const authorizer = new HttpJwtAuthorizer({ + jwtAudience: ['3131231'], + jwtIssuer: 'https://test.us.auth0.com', + }); + + // WHEN + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/books', + authorizer, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Authorizer', { + AuthorizerType: 'JWT', + IdentitySource: ['$request.header.Authorization'], + JwtConfiguration: { + Audience: ['3131231'], + Issuer: 'https://test.us.auth0.com', + }, + }); + }); + + test('same authorizer is used when bound to multiple routes', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + + const authorizer = new HttpJwtAuthorizer({ + jwtAudience: ['3131231'], + jwtIssuer: 'https://test.us.auth0.com', + }); + + // WHEN + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/books', + authorizer, + }); + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/pets', + authorizer, + }); + + // THEN + expect(stack).toCountResources('AWS::ApiGatewayV2::Authorizer', 1); + }); +}); + +class DummyRouteIntegration implements IHttpRouteIntegration { + public bind(_: HttpRouteIntegrationBindOptions) { + return { + payloadFormatVersion: PayloadFormatVersion.VERSION_2_0, + type: HttpIntegrationType.HTTP_PROXY, + uri: 'some-uri', + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/user-pool.test.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/user-pool.test.ts new file mode 100644 index 0000000000000..c12e00a342acd --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/user-pool.test.ts @@ -0,0 +1,83 @@ +import '@aws-cdk/assert/jest'; +import { HttpApi, HttpIntegrationType, HttpRouteIntegrationBindOptions, IHttpRouteIntegration, PayloadFormatVersion } from '@aws-cdk/aws-apigatewayv2'; +import { UserPool } from '@aws-cdk/aws-cognito'; +import { Stack } from '@aws-cdk/core'; +import { HttpUserPoolAuthorizer } from '../../lib'; + +describe('HttpUserPoolAuthorizer', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + const userPool = new UserPool(stack, 'UserPool'); + const userPoolClient = userPool.addClient('UserPoolClient'); + const authorizer = new HttpUserPoolAuthorizer({ + userPool, + userPoolClient, + }); + + // WHEN + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/books', + authorizer, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Authorizer', { + AuthorizerType: 'JWT', + IdentitySource: ['$request.header.Authorization'], + JwtConfiguration: { + Audience: [stack.resolve(userPoolClient.userPoolClientId)], + Issuer: { + 'Fn::Join': [ + '', + [ + 'https://cognito-idp.', + { Ref: 'AWS::Region' }, + '.amazonaws.com/', + stack.resolve(userPool.userPoolId), + ], + ], + }, + }, + }); + }); + + test('same authorizer is used when bound to multiple routes', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + const userPool = new UserPool(stack, 'UserPool'); + const userPoolClient = userPool.addClient('UserPoolClient'); + const authorizer = new HttpUserPoolAuthorizer({ + userPool, + userPoolClient, + }); + + // WHEN + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/books', + authorizer, + }); + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/pets', + authorizer, + }); + + // THEN + expect(stack).toCountResources('AWS::ApiGatewayV2::Authorizer', 1); + }); +}); + +class DummyRouteIntegration implements IHttpRouteIntegration { + public bind(_: HttpRouteIntegrationBindOptions) { + return { + payloadFormatVersion: PayloadFormatVersion.VERSION_2_0, + type: HttpIntegrationType.HTTP_PROXY, + uri: 'some-uri', + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/integ.user-pool.handler/index.ts b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/integ.user-pool.handler/index.ts new file mode 100644 index 0000000000000..afedb7efe3311 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/integ.user-pool.handler/index.ts @@ -0,0 +1,23 @@ +/* eslint-disable no-console */ + +export const handler = async (event: any, _context: any = {}): Promise => { + const authToken: string = event.authorizationToken; + console.log(`event.authorizationToken = ${authToken}`); + if (authToken === 'allow' || authToken === 'deny') { + return { + principalId: 'user', + policyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'execute-api:Invoke', + Effect: authToken, + Resource: event.methodArn, + }, + ], + }, + }; + } else { + throw new Error('Unauthorized'); + } +}; \ No newline at end of file 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 index a052f0243e596..891c7ab616b10 100644 --- 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 @@ -608,24 +608,6 @@ "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": { @@ -633,13 +615,14 @@ "Ref": "HttpProxyPrivateApiA55E154D" }, "RouteKey": "$default", + "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", [ "integrations/", { - "Ref": "HttpProxyPrivateApiDefaultRouteDefaultRouteIntegration0AE210B0" + "Ref": "HttpProxyPrivateApiHttpIntegration1a580b19954e4317026ffbce1f7d5ade82925DF0" } ] ] @@ -664,6 +647,24 @@ "SecurityGroupIds": [] } }, + "HttpProxyPrivateApiHttpIntegration1a580b19954e4317026ffbce1f7d5ade82925DF0": { + "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" + } + }, "HttpProxyPrivateApiDefaultStage18B3706E": { "Type": "AWS::ApiGatewayV2::Stage", "Properties": { diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.http-proxy.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.http-proxy.expected.json index d91a751447c01..71c6674df6b90 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.http-proxy.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.http-proxy.expected.json @@ -37,13 +37,13 @@ "Code": { "ZipFile": "exports.handler = async function(event, context) { return { statusCode: 200, body: \"success\" }; };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "AlwaysSuccessServiceRole6DB8C2F6", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs12.x" }, "DependsOn": [ @@ -94,22 +94,6 @@ } } }, - "LambdaProxyApiDefaultRouteDefaultRouteIntegration97D5250B": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "LambdaProxyApi67594471" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "AlwaysSuccess099EAB05", - "Arn" - ] - }, - "PayloadFormatVersion": "2.0" - } - }, "LambdaProxyApiDefaultRoute1EB30A46": { "Type": "AWS::ApiGatewayV2::Route", "Properties": { @@ -117,19 +101,36 @@ "Ref": "LambdaProxyApi67594471" }, "RouteKey": "$default", + "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", [ "integrations/", { - "Ref": "LambdaProxyApiDefaultRouteDefaultRouteIntegration97D5250B" + "Ref": "LambdaProxyApiHttpIntegration70df0ec52c3e3b6bbc96e64ce3a05f24467606A4" } ] ] } } }, + "LambdaProxyApiHttpIntegration70df0ec52c3e3b6bbc96e64ce3a05f24467606A4": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "LambdaProxyApi67594471" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "AlwaysSuccess099EAB05", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + } + }, "LambdaProxyApiDefaultStage07C38681": { "Type": "AWS::ApiGatewayV2::Stage", "Properties": { @@ -147,7 +148,28 @@ "ProtocolType": "HTTP" } }, - "HttpProxyApiDefaultRouteDefaultRouteIntegrationF2E17850": { + "HttpProxyApiDefaultRoute8AF66B5C": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpProxyApiD0217C67" + }, + "RouteKey": "$default", + "AuthorizationScopes": [], + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpProxyApiHttpIntegration8eeecf9ecdb91f31bebf6bd54fb711a4F32A389A" + } + ] + ] + } + } + }, + "HttpProxyApiHttpIntegration8eeecf9ecdb91f31bebf6bd54fb711a4F32A389A": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { "ApiId": { @@ -178,26 +200,6 @@ "PayloadFormatVersion": "1.0" } }, - "HttpProxyApiDefaultRoute8AF66B5C": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "HttpProxyApiD0217C67" - }, - "RouteKey": "$default", - "Target": { - "Fn::Join": [ - "", - [ - "integrations/", - { - "Ref": "HttpProxyApiDefaultRouteDefaultRouteIntegrationF2E17850" - } - ] - ] - } - } - }, "HttpProxyApiDefaultStageA88F9DE3": { "Type": "AWS::ApiGatewayV2::Stage", "Properties": { diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.lambda-proxy.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.lambda-proxy.expected.json index 38a6929757755..573cd3bcb8bb3 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.lambda-proxy.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.lambda-proxy.expected.json @@ -37,13 +37,13 @@ "Code": { "ZipFile": "exports.handler = async function(event, context) { return { statusCode: 200, body: \"success\" }; };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "AlwaysSuccessServiceRole6DB8C2F6", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs12.x" }, "DependsOn": [ @@ -94,22 +94,6 @@ } } }, - "LambdaProxyApiDefaultRouteDefaultRouteIntegration97D5250B": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "LambdaProxyApi67594471" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "AlwaysSuccess099EAB05", - "Arn" - ] - }, - "PayloadFormatVersion": "2.0" - } - }, "LambdaProxyApiDefaultRoute1EB30A46": { "Type": "AWS::ApiGatewayV2::Route", "Properties": { @@ -117,19 +101,36 @@ "Ref": "LambdaProxyApi67594471" }, "RouteKey": "$default", + "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", [ "integrations/", { - "Ref": "LambdaProxyApiDefaultRouteDefaultRouteIntegration97D5250B" + "Ref": "LambdaProxyApiHttpIntegration70df0ec52c3e3b6bbc96e64ce3a05f24467606A4" } ] ] } } }, + "LambdaProxyApiHttpIntegration70df0ec52c3e3b6bbc96e64ce3a05f24467606A4": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "LambdaProxyApi67594471" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "AlwaysSuccess099EAB05", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + } + }, "LambdaProxyApiDefaultStage07C38681": { "Type": "AWS::ApiGatewayV2::Stage", "Properties": { 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 index ea3801b13354f..6c9fa0633f138 100644 --- 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 @@ -573,24 +573,6 @@ "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": { @@ -598,13 +580,14 @@ "Ref": "HttpProxyPrivateApiA55E154D" }, "RouteKey": "$default", + "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", [ "integrations/", { - "Ref": "HttpProxyPrivateApiDefaultRouteDefaultRouteIntegration0AE210B0" + "Ref": "HttpProxyPrivateApiHttpIntegration1a580b19954e4317026ffbce1f7d5ade82925DF0" } ] ] @@ -629,6 +612,24 @@ "SecurityGroupIds": [] } }, + "HttpProxyPrivateApiHttpIntegration1a580b19954e4317026ffbce1f7d5ade82925DF0": { + "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" + } + }, "HttpProxyPrivateApiDefaultStage18B3706E": { "Type": "AWS::ApiGatewayV2::Stage", "Properties": { 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 index 30e1c20fbaddc..ada88fe7be66f 100644 --- 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 @@ -574,7 +574,28 @@ "ProtocolType": "HTTP" } }, - "HttpProxyPrivateApiDefaultRouteDefaultRouteIntegration0AE210B0": { + "HttpProxyPrivateApiDefaultRoute1BDCA252": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + "RouteKey": "$default", + "AuthorizationScopes": [], + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpProxyPrivateApiHttpIntegrationa5ec5390ca688d567e9449daf58afc6fB75CE02B" + } + ] + ] + } + } + }, + "HttpProxyPrivateApiHttpIntegrationa5ec5390ca688d567e9449daf58afc6fB75CE02B": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { "ApiId": { @@ -595,26 +616,6 @@ "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": { diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 1fda5a731ff40..4da900f271e8f 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -34,6 +34,7 @@ Higher level constructs for Websocket APIs | ![Not Implemented](https://img.shie - [Cross Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors) - [Publishing HTTP APIs](#publishing-http-apis) - [Custom Domain](#custom-domain) + - [Managing access](#managing-access) - [Metrics](#metrics) - [VPC Link](#vpc-link) - [Private Integration](#private-integration) @@ -222,6 +223,13 @@ with 3 API mapping resources across different APIs and Stages. | api | beta | `https://${domainName}/bar` | | apiDemo | $default | `https://${domainName}/demo` | +### Managing access + +API Gateway supports multiple mechanisms for [controlling and managing access to your HTTP +API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-access-control.html) through authorizers. + +These authorizers can be found in the [APIGatewayV2-Authorizers](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigatewayv2-authorizers-readme.html) constructs library. + ## Metrics The API Gateway v2 service sends metrics around the performance of HTTP APIs to Amazon CloudWatch. diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/authorizer.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/authorizer.ts new file mode 100644 index 0000000000000..609d469a572fa --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/authorizer.ts @@ -0,0 +1,12 @@ +import { IResource } from '@aws-cdk/core'; + +/** + * Represents an Authorizer. + */ +export interface IAuthorizer extends IResource { + /** + * Id of the Authorizer + * @attribute + */ + readonly authorizerId: string +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts index d727436b86c99..eeb237a4e7f84 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts @@ -3,3 +3,4 @@ export * from './route'; export * from './stage'; export * from './domain-name'; export * from './api-mapping'; +export * from './authorizer'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index 5ced1f20f18a0..2a481925c0a8b 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -1,9 +1,11 @@ +import * as crypto from 'crypto'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import { Duration, IResource, Resource } from '@aws-cdk/core'; +import { Duration, IResource, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnApi, CfnApiProps } from '../apigatewayv2.generated'; import { DefaultDomainMappingOptions } from '../http/stage'; -import { IHttpRouteIntegration } from './integration'; +import { IHttpRouteAuthorizer } from './authorizer'; +import { IHttpRouteIntegration, HttpIntegration, HttpRouteIntegrationConfig } from './integration'; import { BatchHttpRouteOptions, HttpMethod, HttpRoute, HttpRouteKey } from './route'; import { HttpStage, HttpStageOptions } from './stage'; import { VpcLink, VpcLinkProps } from './vpc-link'; @@ -85,6 +87,12 @@ export interface IHttpApi extends IResource { * Add a new VpcLink */ addVpcLink(options: VpcLinkProps): VpcLink + + /** + * Add a http integration + * @internal + */ + _addIntegration(config: HttpRouteIntegrationConfig): HttpIntegration; } /** @@ -194,6 +202,20 @@ export interface AddRoutesOptions extends BatchHttpRouteOptions { * @default HttpMethod.ANY */ readonly methods?: HttpMethod[]; + + /** + * Authorizer to be associated to these routes. + * @default - No authorizer + */ + readonly authorizer?: IHttpRouteAuthorizer; + + /** + * The list of OIDC scopes to include in the authorization. + * + * These scopes will be merged with the scopes from the attached authorizer + * @default - no additional authorization scopes + */ + readonly authorizationScopes?: string[]; } abstract class HttpApiBase extends Resource implements IHttpApi { // note that this is not exported @@ -201,6 +223,7 @@ abstract class HttpApiBase extends Resource implements IHttpApi { // note that t public abstract readonly httpApiId: string; public abstract readonly apiEndpoint: string; private vpcLinks: Record = {}; + private httpIntegrations: Record = {}; public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { return new cloudwatch.Metric({ @@ -247,6 +270,31 @@ abstract class HttpApiBase extends Resource implements IHttpApi { // note that t return vpcLink; } + + /** + * @internal + */ + public _addIntegration(config: HttpRouteIntegrationConfig): HttpIntegration { + const stringifiedConfig = JSON.stringify(Stack.of(this).resolve(config)); + const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); + + if (configHash in this.httpIntegrations) { + return this.httpIntegrations[configHash]; + } + + const integration = new HttpIntegration(this, `HttpIntegration-${configHash}`, { + httpApi: this, + integrationType: config.type, + integrationUri: config.uri, + method: config.method, + connectionId: config.connectionId, + connectionType: config.connectionType, + payloadFormatVersion: config.payloadFormatVersion, + }); + this.httpIntegrations[configHash] = integration; + + return integration; + } } /** @@ -413,6 +461,8 @@ export class HttpApi extends HttpApiBase { httpApi: this, routeKey: HttpRouteKey.with(options.path, method), integration: options.integration, + authorizer: options.authorizer, + authorizationScopes: options.authorizationScopes, })); } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts new file mode 100644 index 0000000000000..aadfb630ba276 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts @@ -0,0 +1,174 @@ +import { Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnAuthorizer } from '../apigatewayv2.generated'; + +import { IAuthorizer } from '../common'; +import { IHttpApi } from './api'; +import { IHttpRoute } from './route'; + +/** + * Supported Authorizer types + */ +export enum HttpAuthorizerType { + /** JSON Web Tokens */ + JWT = 'JWT', + + /** Lambda Authorizer */ + LAMBDA = 'REQUEST', +} + +/** + * Properties to initialize an instance of `HttpAuthorizer`. + */ +export interface HttpAuthorizerProps { + /** + * Name of the authorizer + * @default - id of the HttpAuthorizer construct. + */ + readonly authorizerName?: string + + /** + * HTTP Api to attach the authorizer to + */ + readonly httpApi: IHttpApi + + /** + * The type of authorizer + */ + readonly type: HttpAuthorizerType; + + /** + * The identity source for which authorization is requested. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-authorizer.html#cfn-apigatewayv2-authorizer-identitysource + */ + readonly identitySource: string[]; + + /** + * A list of the intended recipients of the JWT. + * A valid JWT must provide an aud that matches at least one entry in this list. + * @default - required for JWT authorizer typess. + */ + readonly jwtAudience?: string[] + + /** + * The base domain of the identity provider that issues JWT. + * @default - required for JWT authorizer types. + */ + readonly jwtIssuer?: string; +} + +/** + * An authorizer for HTTP APIs + */ +export interface IHttpAuthorizer extends IAuthorizer { +} + +/** + * Reference to an http authorizer + */ +export interface HttpAuthorizerAttributes { + /** + * Id of the Authorizer + */ + readonly authorizerId: string + + /** + * Type of authorizer + */ + readonly authorizerType: HttpAuthorizerType +} + +/** + * An authorizer for Http Apis + * @resource AWS::ApiGatewayV2::Authorizer + */ +export class HttpAuthorizer extends Resource implements IHttpAuthorizer { + /** + * Import an existing HTTP Authorizer into this CDK app. + */ + public static fromHttpAuthorizerAttributes(scope: Construct, id: string, attrs: HttpAuthorizerAttributes): IHttpRouteAuthorizer { + class Import extends Resource implements IHttpRouteAuthorizer { + public readonly authorizerId = attrs.authorizerId; + public readonly authorizerType = attrs.authorizerType; + + public bind(): HttpRouteAuthorizerConfig { + return { + authorizerId: attrs.authorizerId, + authorizationType: attrs.authorizerType, + }; + } + } + return new Import(scope, id); + } + + public readonly authorizerId: string; + + constructor(scope: Construct, id: string, props: HttpAuthorizerProps) { + super(scope, id); + + if (props.type === HttpAuthorizerType.JWT && (!props.jwtAudience || props.jwtAudience.length === 0 || !props.jwtIssuer)) { + throw new Error('jwtAudience and jwtIssuer are mandatory for JWT authorizers'); + } + + const resource = new CfnAuthorizer(this, 'Resource', { + name: props.authorizerName ?? id, + apiId: props.httpApi.httpApiId, + authorizerType: props.type, + identitySource: props.identitySource, + jwtConfiguration: undefinedIfNoKeys({ + audience: props.jwtAudience, + issuer: props.jwtIssuer, + }), + }); + + this.authorizerId = resource.ref; + } +} + +/** + * Input to the bind() operation, that binds an authorizer to a route. + */ +export interface HttpRouteAuthorizerBindOptions { + /** + * The route to which the authorizer is being bound. + */ + readonly route: IHttpRoute; + /** + * The scope for any constructs created as part of the bind. + */ + readonly scope: Construct; +} + +/** + * Results of binding an authorizer to an http route. + */ +export interface HttpRouteAuthorizerConfig { + /** + * The authorizer id + */ + readonly authorizerId: string; + /** + * The type of authorization + */ + readonly authorizationType: HttpAuthorizerType; + /** + * The list of OIDC scopes to include in the authorization. + * @default - no authorization scopes + */ + readonly authorizationScopes?: string[]; +} + +/** + * An authorizer that can attach to an Http Route. + */ +export interface IHttpRouteAuthorizer { + /** + * Bind this authorizer to a specified Http route. + */ + bind(options: HttpRouteAuthorizerBindOptions): HttpRouteAuthorizerConfig; +} + +function undefinedIfNoKeys(obj: A): A | undefined { + const allUndefined = Object.values(obj).every(val => val === undefined); + return allUndefined ? undefined : obj; +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts index c594da33bac91..efd60f9f24d7c 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 './stage'; export * from './api-mapping'; export * from './vpc-link'; +export * from './authorizer'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts index e688e78d84921..dac962f7bfee4 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts @@ -3,7 +3,8 @@ import { Construct } from 'constructs'; import { CfnRoute, CfnRouteProps } from '../apigatewayv2.generated'; import { IRoute } from '../common'; import { IHttpApi } from './api'; -import { HttpIntegration, IHttpRouteIntegration } from './integration'; +import { IHttpRouteAuthorizer } from './authorizer'; +import { IHttpRouteIntegration } from './integration'; /** * Represents a Route for an HTTP API. @@ -103,6 +104,20 @@ export interface HttpRouteProps extends BatchHttpRouteOptions { * The key to this route. This is a combination of an HTTP method and an HTTP path. */ readonly routeKey: HttpRouteKey; + + /** + * Authorizer for a WebSocket API or an HTTP API. + * @default - No authorizer + */ + readonly authorizer?: IHttpRouteAuthorizer; + + /** + * The list of OIDC scopes to include in the authorization. + * + * These scopes will be merged with the scopes from the attached authorizer + * @default - no additional authorization scopes + */ + readonly authorizationScopes?: string[]; } /** @@ -125,20 +140,29 @@ export class HttpRoute extends Resource implements IHttpRoute { scope: this, }); - 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, - }); + const integration = props.httpApi._addIntegration(config); + + const authBindResult = props.authorizer ? props.authorizer.bind({ + route: this, + scope: this.httpApi instanceof Construct ? this.httpApi : this, // scope under the API if it's not imported + }) : undefined; + + let authorizationScopes = authBindResult?.authorizationScopes ?? []; + + if (authBindResult && props.authorizationScopes) { + authorizationScopes = Array.from(new Set([ + ...authorizationScopes, + ...props.authorizationScopes, + ])); + } const routeProps: CfnRouteProps = { apiId: props.httpApi.httpApiId, routeKey: props.routeKey.key, target: `integrations/${integration.integrationId}`, + authorizerId: authBindResult?.authorizerId, + authorizationType: authBindResult?.authorizationType, + authorizationScopes, }; const route = new CfnRoute(this, 'Resource', routeProps); diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 68a56a7acb216..53cfaeefc2515 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -77,6 +77,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", 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 70bc45000ddec..01252be7d84f1 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -3,7 +3,10 @@ import { ABSENT } from '@aws-cdk/assert'; import { Metric } from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import { Duration, Stack } from '@aws-cdk/core'; -import { HttpApi, HttpIntegrationType, HttpMethod, HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion } from '../../lib'; +import { + HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpIntegrationType, HttpMethod, HttpRouteAuthorizerBindOptions, HttpRouteAuthorizerConfig, + HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, +} from '../../lib'; describe('HttpApi', () => { test('default', () => { @@ -274,6 +277,83 @@ describe('HttpApi', () => { expect(api.apiEndpoint).toBeDefined(); }); + test('can attach authorizer to route', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'api'); + + const authorizer = new DummyAuthorizer(); + + httpApi.addRoutes({ + path: '/pets', + integration: new DummyRouteIntegration(), + authorizer, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Api', { + Name: 'api', + ProtocolType: 'HTTP', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + AuthorizerId: 'auth-1234', + AuthorizationType: 'JWT', + }); + }); + + test('can import existing authorizer and attach to route', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'HttpApi'); + + const authorizer = HttpAuthorizer.fromHttpAuthorizerAttributes(stack, 'auth', { + authorizerId: '12345', + authorizerType: HttpAuthorizerType.JWT, + }); + + // WHEN + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/books', + authorizer, + }); + + api.addRoutes({ + integration: new DummyRouteIntegration(), + path: '/pets', + authorizer, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + AuthorizerId: '12345', + }); + }); + + test('can attach custom scopes to authorizer route', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'api'); + + const authorizer = new DummyAuthorizer(); + + httpApi.addRoutes({ + path: '/pets', + integration: new DummyRouteIntegration(), + authorizer, + authorizationScopes: ['read:scopes'], + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Api', { + Name: 'api', + ProtocolType: 'HTTP', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + AuthorizerId: 'auth-1234', + AuthorizationType: 'JWT', + AuthorizationScopes: ['read:scopes'], + }); + }); + test('throws when accessing apiEndpoint and disableExecuteApiEndpoint is true', () => { const stack = new Stack(); const api = new HttpApi(stack, 'api', { @@ -301,4 +381,13 @@ class DummyRouteIntegration implements IHttpRouteIntegration { uri: 'some-uri', }; } +} + +class DummyAuthorizer implements IHttpRouteAuthorizer { + public bind(_: HttpRouteAuthorizerBindOptions): HttpRouteAuthorizerConfig { + return { + authorizerId: 'auth-1234', + authorizationType: HttpAuthorizerType.JWT, + }; + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/authorizer.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/authorizer.test.ts new file mode 100644 index 0000000000000..bee2f7d05be1b --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/authorizer.test.ts @@ -0,0 +1,67 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { + HttpApi, HttpAuthorizer, HttpAuthorizerType, +} from '../../lib'; + +describe('HttpAuthorizer', () => { + test('default', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + new HttpAuthorizer(stack, 'HttpAuthorizer', { + httpApi, + identitySource: ['identitysource.1', 'identitysource.2'], + type: HttpAuthorizerType.JWT, + jwtAudience: ['audience.1', 'audience.2'], + jwtIssuer: 'issuer', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Authorizer', { + ApiId: stack.resolve(httpApi.httpApiId), + Name: 'HttpAuthorizer', + AuthorizerType: 'JWT', + IdentitySource: ['identitysource.1', 'identitysource.2'], + }); + }); + + test('authorizer name', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + new HttpAuthorizer(stack, 'HttpAuthorizer', { + httpApi, + authorizerName: 'my-authorizer', + identitySource: ['identitysource.1', 'identitysource.2'], + type: HttpAuthorizerType.JWT, + jwtAudience: ['audience.1', 'audience.2'], + jwtIssuer: 'issuer', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Authorizer', { + Name: 'my-authorizer', + }); + }); + + describe('jwt configuration', () => { + test('audience and issuer', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + new HttpAuthorizer(stack, 'HttpAuthorizer', { + httpApi, + identitySource: ['identitysource.1', 'identitysource.2'], + type: HttpAuthorizerType.JWT, + jwtAudience: ['audience.1', 'audience.2'], + jwtIssuer: 'issuer', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Authorizer', { + JwtConfiguration: { + Audience: ['audience.1', 'audience.2'], + Issuer: 'issuer', + }, + }); + }); + }); +}); 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 e250ae153389d..bfeb036a3fa72 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts @@ -1,8 +1,8 @@ import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; import { - HttpApi, HttpConnectionType, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteIntegration, - PayloadFormatVersion, + HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpConnectionType, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteAuthorizerBindOptions, + HttpRouteAuthorizerConfig, HttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, } from '../../lib'; describe('HttpRoute', () => { @@ -25,7 +25,7 @@ describe('HttpRoute', () => { [ 'integrations/', { - Ref: 'HttpRouteHttpRouteIntegration6EE0FE47', + Ref: 'HttpApiHttpIntegrationcff2618c192d3bd8581dd2a4093464f6CDB667B8', }, ], ], @@ -55,6 +55,66 @@ describe('HttpRoute', () => { }); }); + test('integration is only configured once if multiple routes are configured with it', () => { + // GIVEN + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + const integration = new DummyIntegration(); + + // WHEN + new HttpRoute(stack, 'HttpRoute1', { + httpApi, + integration, + routeKey: HttpRouteKey.with('/books', HttpMethod.GET), + }); + new HttpRoute(stack, 'HttpRoute2', { + httpApi, + integration, + routeKey: HttpRouteKey.with('/books', HttpMethod.POST), + }); + + // THEN + expect(stack).toCountResources('AWS::ApiGatewayV2::Integration', 1); + }); + + test('integration can be used across HttpApis', () => { + // GIVEN + const integration = new DummyIntegration(); + + // WHEN + const stack1 = new Stack(); + const httpApi1 = new HttpApi(stack1, 'HttpApi1'); + + new HttpRoute(stack1, 'HttpRoute1', { + httpApi: httpApi1, + integration, + routeKey: HttpRouteKey.with('/books', HttpMethod.GET), + }); + new HttpRoute(stack1, 'HttpRoute2', { + httpApi: httpApi1, + integration, + routeKey: HttpRouteKey.with('/books', HttpMethod.POST), + }); + + const stack2 = new Stack(); + const httpApi2 = new HttpApi(stack2, 'HttpApi2'); + + new HttpRoute(stack2, 'HttpRoute1', { + httpApi: httpApi2, + integration, + routeKey: HttpRouteKey.with('/books', HttpMethod.GET), + }); + new HttpRoute(stack2, 'HttpRoute2', { + httpApi: httpApi2, + integration, + routeKey: HttpRouteKey.with('/books', HttpMethod.POST), + }); + + // THEN + expect(stack1).toCountResources('AWS::ApiGatewayV2::Integration', 1); + expect(stack2).toCountResources('AWS::ApiGatewayV2::Integration', 1); + }); + test('throws when path not start with /', () => { const stack = new Stack(); const httpApi = new HttpApi(stack, 'HttpApi'); @@ -113,6 +173,53 @@ describe('HttpRoute', () => { }); expect(stack).not.toHaveResource('AWS::ApiGatewayV2::VpcLink'); }); + + test('can create route with an authorizer attached', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + const authorizer = new DummyAuthorizer(); + + const route = new HttpRoute(stack, 'HttpRoute', { + httpApi, + integration: new DummyIntegration(), + routeKey: HttpRouteKey.with('/books', HttpMethod.GET), + authorizer, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + ApiId: stack.resolve(httpApi.httpApiId), + IntegrationType: 'HTTP_PROXY', + PayloadFormatVersion: '2.0', + IntegrationUri: 'some-uri', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Authorizer'); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + AuthorizerId: stack.resolve(authorizer.bind({ scope: stack, route: route }).authorizerId), + AuthorizationType: 'JWT', + }); + }); + + test('can attach additional scopes to a route with an authorizer attached', () => { + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + const authorizer = new DummyAuthorizer(); + + new HttpRoute(stack, 'HttpRoute', { + httpApi, + integration: new DummyIntegration(), + routeKey: HttpRouteKey.with('/books', HttpMethod.GET), + authorizer, + authorizationScopes: ['read:books'], + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + AuthorizationScopes: ['read:books'], + }); + }); }); class DummyIntegration implements IHttpRouteIntegration { @@ -124,4 +231,26 @@ class DummyIntegration implements IHttpRouteIntegration { method: HttpMethod.DELETE, }; } +} + +class DummyAuthorizer implements IHttpRouteAuthorizer { + private authorizer?: HttpAuthorizer; + + public bind(options: HttpRouteAuthorizerBindOptions): HttpRouteAuthorizerConfig { + if (!this.authorizer) { + + this.authorizer = new HttpAuthorizer(options.scope, 'auth-1234', { + httpApi: options.route.httpApi, + identitySource: ['identitysource.1', 'identitysource.2'], + type: HttpAuthorizerType.JWT, + jwtAudience: ['audience.1', 'audience.2'], + jwtIssuer: 'issuer', + }); + } + + return { + authorizerId: this.authorizer.authorizerId, + authorizationType: HttpAuthorizerType.JWT, + }; + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/base-scalable-attribute.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/base-scalable-attribute.ts index 762da275c33a5..95d2a19c8a17d 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/base-scalable-attribute.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/base-scalable-attribute.ts @@ -58,7 +58,7 @@ export abstract class BaseScalableAttribute extends CoreConstruct { scalableDimension: this.props.dimension, resourceId: this.props.resourceId, role: this.props.role, - minCapacity: props.minCapacity !== undefined ? props.minCapacity : 1, + minCapacity: props.minCapacity ?? 1, maxCapacity: props.maxCapacity, }); } diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index 86d5f3f272bf2..ae135c546f89c 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -74,7 +74,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "fast-check": "^2.11.0", + "fast-check": "^2.12.1", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 2253b12d2987d..c400cbb0af05d 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -109,23 +109,21 @@ When creating a virtual service: Adding a virtual router as the provider: ```ts -mesh.addVirtualService('virtual-service', { - virtualRouter: router, - virtualServiceName: 'my-service.default.svc.cluster.local', +new appmesh.VirtualService('virtual-service', { + virtualServiceName: 'my-service.default.svc.cluster.local', // optional + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualRouter(router), }); ``` Adding a virtual node as the provider: ```ts -mesh.addVirtualService('virtual-service', { - virtualNode: node, - virtualServiceName: `my-service.default.svc.cluster.local`, +new appmesh.VirtualService('virtual-service', { + virtualServiceName: `my-service.default.svc.cluster.local`, // optional + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualNode(node), }); ``` -**Note** that only one must of `virtualNode` or `virtualRouter` must be chosen. - ## Adding a VirtualNode A `virtual node` acts as a logical pointer to a particular task group, such as an Amazon ECS service or a Kubernetes deployment. diff --git a/packages/@aws-cdk/aws-appmesh/lib/mesh.ts b/packages/@aws-cdk/aws-appmesh/lib/mesh.ts index cc0596695aae2..a1cd5b49904a5 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/mesh.ts @@ -4,7 +4,6 @@ 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'; /** * A utility enum defined for the egressFilter type property, the default of DROP_ALL, @@ -46,11 +45,6 @@ export interface IMesh extends cdk.IResource { */ addVirtualRouter(id: string, props?: VirtualRouterBaseProps): VirtualRouter; - /** - * Adds a VirtualService with the given id - */ - addVirtualService(id: string, props?: VirtualServiceBaseProps): VirtualService; - /** * Adds a VirtualNode to the Mesh */ @@ -86,16 +80,6 @@ abstract class MeshBase extends cdk.Resource implements IMesh { }); } - /** - * Adds a VirtualService with the given id - */ - public addVirtualService(id: string, props: VirtualServiceBaseProps = {}): VirtualService { - return new VirtualService(this, id, { - ...props, - mesh: this, - }); - } - /** * Adds a VirtualNode to the Mesh */ diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts index 9ca5d5010b7f4..5685b8b08c1f8 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts @@ -36,9 +36,9 @@ export interface IVirtualService extends cdk.IResource { } /** - * The base properties which all classes in VirtualService will inherit from + * The properties applied to the VirtualService being defined */ -export interface VirtualServiceBaseProps { +export interface VirtualServiceProps { /** * The name of the VirtualService. * @@ -50,36 +50,17 @@ export interface VirtualServiceBaseProps { */ readonly virtualServiceName?: string; - /** - * The VirtualRouter which the VirtualService uses as provider - * - * @default - At most one of virtualRouter and virtualNode is allowed. - */ - readonly virtualRouter?: IVirtualRouter; - - /** - * The VirtualNode attached to the virtual service - * - * @default - At most one of virtualRouter and virtualNode is allowed. - */ - readonly virtualNode?: IVirtualNode; - /** * Client policy for this Virtual Service * * @default - none */ readonly clientPolicy?: ClientPolicy; -} -/** - * The properties applied to the VirtualService being define - */ -export interface VirtualServiceProps extends VirtualServiceBaseProps { /** - * The Mesh which the VirtualService belongs to + * The VirtualNode or VirtualRouter which the VirtualService uses as its provider */ - readonly mesh: IMesh; + readonly virtualServiceProvider: VirtualServiceProvider; } /** @@ -135,59 +116,35 @@ export class VirtualService extends cdk.Resource implements IVirtualService { public readonly clientPolicy?: ClientPolicy; - private readonly virtualServiceProvider?: CfnVirtualService.VirtualServiceProviderProperty; - constructor(scope: Construct, id: string, props: VirtualServiceProps) { super(scope, id, { physicalName: props.virtualServiceName || cdk.Lazy.string({ produce: () => cdk.Names.uniqueId(this) }), }); - if (props.virtualNode && props.virtualRouter) { - throw new Error('Must provide only one of virtualNode or virtualRouter for the provider'); - } - - this.mesh = props.mesh; this.clientPolicy = props.clientPolicy; - - // Check which provider to use node or router (or neither) - if (props.virtualRouter) { - this.virtualServiceProvider = this.addVirtualRouter(props.virtualRouter.virtualRouterName); - } - if (props.virtualNode) { - this.virtualServiceProvider = this.addVirtualNode(props.virtualNode.virtualNodeName); - } + const providerConfig = props.virtualServiceProvider.bind(this); + this.mesh = providerConfig.mesh; const svc = new CfnVirtualService(this, 'Resource', { meshName: this.mesh.meshName, virtualServiceName: this.physicalName, spec: { - provider: this.virtualServiceProvider, + provider: providerConfig.virtualNodeProvider || providerConfig.virtualRouterProvider + ? { + virtualNode: providerConfig.virtualNodeProvider, + virtualRouter: providerConfig.virtualRouterProvider, + } + : undefined, }, }); this.virtualServiceName = this.getResourceNameAttribute(svc.attrVirtualServiceName); this.virtualServiceArn = this.getResourceArnAttribute(svc.ref, { service: 'appmesh', - resource: `mesh/${props.mesh.meshName}/virtualService`, + resource: `mesh/${this.mesh.meshName}/virtualService`, resourceName: this.physicalName, }); } - - private addVirtualRouter(name: string): CfnVirtualService.VirtualServiceProviderProperty { - return { - virtualRouter: { - virtualRouterName: name, - }, - }; - } - - private addVirtualNode(name: string): CfnVirtualService.VirtualServiceProviderProperty { - return { - virtualNode: { - virtualNodeName: name, - }, - }; - } } /** @@ -211,3 +168,91 @@ export interface VirtualServiceAttributes { */ readonly clientPolicy?: ClientPolicy; } + +/** + * Properties for a VirtualService provider + */ +export interface VirtualServiceProviderConfig { + /** + * Virtual Node based provider + * + * @default - none + */ + readonly virtualNodeProvider?: CfnVirtualService.VirtualNodeServiceProviderProperty; + + /** + * Virtual Router based provider + * + * @default - none + */ + readonly virtualRouterProvider?: CfnVirtualService.VirtualRouterServiceProviderProperty; + + /** + * Mesh the Provider is using + * + * @default - none + */ + readonly mesh: IMesh; +} + +/** + * Represents the properties needed to define the provider for a VirtualService + */ +export abstract class VirtualServiceProvider { + /** + * Returns a VirtualNode based Provider for a VirtualService + */ + public static virtualNode(virtualNode: IVirtualNode): VirtualServiceProvider { + return new VirtualServiceProviderImpl(virtualNode, undefined); + } + + /** + * Returns a VirtualRouter based Provider for a VirtualService + */ + public static virtualRouter(virtualRouter: IVirtualRouter): VirtualServiceProvider { + return new VirtualServiceProviderImpl(undefined, virtualRouter); + } + + /** + * Returns an Empty Provider for a VirtualService. This provides no routing capabilities + * and should only be used as a placeholder + */ + public static none(mesh: IMesh): VirtualServiceProvider { + return new VirtualServiceProviderImpl(undefined, undefined, mesh); + } + + /** + * Enforces mutual exclusivity for VirtualService provider types. + */ + public abstract bind(_construct: Construct): VirtualServiceProviderConfig; +} + +class VirtualServiceProviderImpl extends VirtualServiceProvider { + private readonly virtualNode?: IVirtualNode; + private readonly virtualRouter?: IVirtualRouter; + private readonly mesh: IMesh; + + constructor(virtualNode?: IVirtualNode, virtualRouter?: IVirtualRouter, mesh?: IMesh) { + super(); + this.virtualNode = virtualNode; + this.virtualRouter = virtualRouter; + const providedMesh = this.virtualNode?.mesh ?? this.virtualRouter?.mesh ?? mesh!; + this.mesh = providedMesh; + } + + public bind(_construct: Construct): VirtualServiceProviderConfig { + return { + mesh: this.mesh, + virtualNodeProvider: this.virtualNode + ? { + virtualNodeName: this.virtualNode.virtualNodeName, + } + : undefined, + virtualRouterProvider: this.virtualRouter + ? { + virtualRouterName: this.virtualRouter.virtualRouterName, + } + : undefined, + }; + } +} 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 db9277d071432..5f4a9ca206725 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -625,41 +625,6 @@ } } }, - "meshserviceE06ECED5": { - "Type": "AWS::AppMesh::VirtualService", - "Properties": { - "MeshName": { - "Fn::GetAtt": [ - "meshACDFE68E", - "MeshName" - ] - }, - "Spec": { - "Provider": { - "VirtualRouter": { - "VirtualRouterName": { - "Fn::GetAtt": [ - "meshrouter81B8087E", - "VirtualRouterName" - ] - } - } - } - }, - "VirtualServiceName": "service1.domain.local" - } - }, - "cert56CA94EB": { - "Type": "AWS::CertificateManager::Certificate", - "Properties": { - "DomainName":"node1.domain.local", - "DomainValidationOptions": [{ - "DomainName":"node1.domain.local", - "ValidationDomain":"local" - }], - "ValidationMethod": "EMAIL" - } - }, "meshnode726C787D": { "Type": "AWS::AppMesh::VirtualNode", "Properties": { @@ -675,7 +640,7 @@ "VirtualService": { "VirtualServiceName": { "Fn::GetAtt": [ - "meshserviceE06ECED5", + "service6D174F83", "VirtualServiceName" ] } @@ -706,16 +671,6 @@ "PortMapping": { "Port": 8080, "Protocol": "http" - }, - "TLS": { - "Certificate": { - "ACM": { - "CertificateArn": { - "Ref": "cert56CA94EB" - } - } - }, - "Mode": "STRICT" } } ], @@ -743,8 +698,8 @@ "TLS": { "Validation": { "Trust": { - "ACM": { - "CertificateAuthorityArns": ["arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012"] + "File": { + "CertificateChain": "path/to/cert" } } } @@ -891,7 +846,7 @@ "VirtualService": { "VirtualServiceName": { "Fn::GetAtt": [ - "meshserviceE06ECED5", + "service6D174F83", "VirtualServiceName" ] } @@ -928,7 +883,7 @@ "VirtualService": { "VirtualServiceName": { "Fn::GetAtt": [ - "meshserviceE06ECED5", + "service6D174F83", "VirtualServiceName" ] } @@ -965,7 +920,7 @@ "VirtualService": { "VirtualServiceName": { "Fn::GetAtt": [ - "meshserviceE06ECED5", + "service6D174F83", "VirtualServiceName" ] } @@ -975,7 +930,7 @@ "Match": { "ServiceName": { "Fn::GetAtt": [ - "meshserviceE06ECED5", + "service6D174F83", "VirtualServiceName" ] } @@ -990,6 +945,30 @@ } } }, + "service6D174F83": { + "Type": "AWS::AppMesh::VirtualService", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Provider": { + "VirtualRouter": { + "VirtualRouterName": { + "Fn::GetAtt": [ + "meshrouter81B8087E", + "VirtualRouterName" + ] + } + } + } + }, + "VirtualServiceName": "service1.domain.local" + } + }, "service27C65CF7D": { "Type": "AWS::AppMesh::VirtualService", "Properties": { diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index 730418aef7970..90e54586f7f51 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -1,5 +1,3 @@ -import * as acmpca from '@aws-cdk/aws-acmpca'; -import * as acm from '@aws-cdk/aws-certificatemanager'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; @@ -25,15 +23,11 @@ const router = mesh.addVirtualRouter('router', { ], }); -const virtualService = mesh.addVirtualService('service', { - virtualRouter: router, +const virtualService = new appmesh.VirtualService(stack, 'service', { + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualRouter(router), virtualServiceName: 'service1.domain.local', }); -const cert = new acm.Certificate(stack, 'cert', { - domainName: `node1.${namespace.namespaceName}`, -}); - const node = mesh.addVirtualNode('node', { serviceDiscovery: appmesh.ServiceDiscovery.dns(`node1.${namespace.namespaceName}`), listeners: [appmesh.VirtualNodeListener.http({ @@ -41,10 +35,6 @@ const node = mesh.addVirtualNode('node', { healthyThreshold: 3, path: '/check-path', }, - tlsCertificate: appmesh.TlsCertificate.acm({ - certificate: cert, - tlsMode: appmesh.TlsMode.STRICT, - }), })], backends: [ virtualService, @@ -53,7 +43,7 @@ const node = mesh.addVirtualNode('node', { node.addBackend(new appmesh.VirtualService(stack, 'service-2', { virtualServiceName: 'service2.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }), ); @@ -75,8 +65,6 @@ router.addRoute('route-1', { }), }); -const certificateAuthorityArn = 'arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012'; - const node2 = mesh.addVirtualNode('node2', { serviceDiscovery: appmesh.ServiceDiscovery.dns(`node2.${namespace.namespaceName}`), listeners: [appmesh.VirtualNodeListener.http({ @@ -90,13 +78,13 @@ const node2 = mesh.addVirtualNode('node2', { unhealthyThreshold: 2, }, })], - backendsDefaultClientPolicy: appmesh.ClientPolicy.acmTrust({ - certificateAuthorities: [acmpca.CertificateAuthority.fromCertificateAuthorityArn(stack, 'certificate', certificateAuthorityArn)], + backendsDefaultClientPolicy: appmesh.ClientPolicy.fileTrust({ + certificateChain: 'path/to/cert', }), backends: [ new appmesh.VirtualService(stack, 'service-3', { virtualServiceName: 'service3.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }), ], }); diff --git a/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts index a741ec0b0d1d8..fed290d36a3e2 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts @@ -21,7 +21,7 @@ export = { }); const virtualService = new appmesh.VirtualService(stack, 'vs-1', { - mesh: mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), virtualServiceName: 'target.local', }); @@ -121,7 +121,9 @@ export = { meshName: 'test-mesh', }); - const virtualService = mesh.addVirtualService('testVirtualService'); + const virtualService = new appmesh.VirtualService(stack, 'testVirtualService', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + }); test.throws(() => appmesh.GatewayRouteSpec.http({ routeTarget: virtualService, match: { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts index c6d2fbfbbb294..ce50c1402a7c3 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts @@ -125,120 +125,6 @@ export = { test.done(); }, - 'When adding a VirtualService to a mesh': { - 'with VirtualRouter and VirtualNode as providers': { - 'should throw error'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const mesh = new appmesh.Mesh(stack, 'mesh', { - meshName: 'test-mesh', - }); - - const testNode = new appmesh.VirtualNode(stack, 'test-node', { - mesh, - serviceDiscovery: appmesh.ServiceDiscovery.dns('test-node'), - }); - - const testRouter = mesh.addVirtualRouter('router', { - listeners: [ - appmesh.VirtualRouterListener.http(), - ], - }); - - // THEN - test.throws(() => { - mesh.addVirtualService('service', { - virtualServiceName: 'test-service.domain.local', - virtualNode: testNode, - virtualRouter: testRouter, - }); - }); - - test.done(); - }, - }, - 'with single virtual router provider resource': { - 'should create service'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const mesh = new appmesh.Mesh(stack, 'mesh', { - meshName: 'test-mesh', - }); - - const testRouter = mesh.addVirtualRouter('test-router', { - listeners: [ - appmesh.VirtualRouterListener.http(), - ], - }); - - mesh.addVirtualService('service', { - virtualServiceName: 'test-service.domain.local', - virtualRouter: testRouter, - }); - - // THEN - expect(stack).to( - haveResource('AWS::AppMesh::VirtualService', { - Spec: { - Provider: { - VirtualRouter: { - VirtualRouterName: { - 'Fn::GetAtt': ['meshtestrouterF78D72DD', 'VirtualRouterName'], - }, - }, - }, - }, - }), - ); - - test.done(); - }, - }, - 'with single virtual node provider resource': { - 'should create service'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const mesh = new appmesh.Mesh(stack, 'mesh', { - meshName: 'test-mesh', - }); - - const node = mesh.addVirtualNode('test-node', { - serviceDiscovery: appmesh.ServiceDiscovery.dns('test.domain.local'), - listeners: [appmesh.VirtualNodeListener.http({ - port: 8080, - })], - }); - - mesh.addVirtualService('service2', { - virtualServiceName: 'test-service.domain.local', - virtualNode: node, - }); - - // THEN - expect(stack).to( - haveResource('AWS::AppMesh::VirtualService', { - Spec: { - Provider: { - VirtualNode: { - VirtualNodeName: { - 'Fn::GetAtt': ['meshtestnodeF93946D4', 'VirtualNodeName'], - }, - }, - }, - }, - }), - ); - - test.done(); - }, - }, - }, 'When adding a VirtualNode to a mesh': { 'with empty default listeners and backends': { 'should create default resource'(test: Test) { @@ -376,7 +262,7 @@ export = { const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); mesh.addVirtualNode('test-node', { @@ -415,8 +301,9 @@ export = { const stack2 = new cdk.Stack(); const mesh2 = appmesh.Mesh.fromMeshName(stack2, 'imported-mesh', 'abc'); - mesh2.addVirtualService('service', { + new appmesh.VirtualService(stack2, 'service', { virtualServiceName: 'test.domain.local', + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh2), }); // THEN diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts index 7b4a563c90d34..b9d3ed70cae43 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts @@ -306,7 +306,9 @@ export = { mesh: mesh, }); - const virtualService = mesh.addVirtualService('virtualService', {}); + const virtualService = new appmesh.VirtualService(stack, 'virtualService', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + }); virtualGateway.addGatewayRoute('testGatewayRoute', { gatewayRouteName: 'test-gateway-route', @@ -324,7 +326,7 @@ export = { Target: { VirtualService: { VirtualServiceName: { - 'Fn::GetAtt': ['meshvirtualService93460D43', 'VirtualServiceName'], + 'Fn::GetAtt': ['virtualService03A04B87', 'VirtualServiceName'], }, }, }, @@ -349,7 +351,9 @@ export = { meshName: 'test-mesh', }); - const virtualService = mesh.addVirtualService('virtualService', {}); + const virtualService = new appmesh.VirtualService(stack, 'virtualService', { + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + }); const virtualGateway = mesh.addVirtualGateway('gateway'); virtualGateway.addGatewayRoute('testGatewayRoute', { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts index 9fb05931a2a44..4337973230854 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts @@ -19,11 +19,11 @@ export = { const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); const service2 = new appmesh.VirtualService(stack, 'service-2', { virtualServiceName: 'service2.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); const node = new appmesh.VirtualNode(stack, 'test-node', { @@ -319,7 +319,7 @@ export = { const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), clientPolicy: appmesh.ClientPolicy.fileTrust({ certificateChain: 'path-to-certificate', ports: [8080, 8081], diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts index aafe3dff8ce7f..2732adb4cba17 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts @@ -101,7 +101,7 @@ export = { const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); const node = mesh.addVirtualNode('test-node', { @@ -170,11 +170,11 @@ export = { const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); const service2 = new appmesh.VirtualService(stack, 'service-2', { virtualServiceName: 'service2.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); const node = mesh.addVirtualNode('test-node', { @@ -332,7 +332,7 @@ export = { const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', - mesh, + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); const node = mesh.addVirtualNode('test-node', { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts index c09c156ac75ea..c60c98f8e7b94 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts @@ -1,3 +1,4 @@ +import { expect, haveResource } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; @@ -19,6 +20,7 @@ export = { test.done(); }, + 'Can import Virtual Services using attributes'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -34,6 +36,90 @@ export = { // THEN test.equal(virtualService.mesh.meshName, meshName); test.equal(virtualService.virtualServiceName, virtualServiceName); + test.done(); }, + + 'When adding a VirtualService to a mesh': { + 'with single virtual router provider resource': { + 'should create service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const testRouter = mesh.addVirtualRouter('test-router', { + listeners: [ + appmesh.VirtualRouterListener.http(), + ], + }); + + new appmesh.VirtualService(stack, 'service', { + virtualServiceName: 'test-service.domain.local', + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualRouter(testRouter), + }); + + // THEN + expect(stack).to( + haveResource('AWS::AppMesh::VirtualService', { + Spec: { + Provider: { + VirtualRouter: { + VirtualRouterName: { + 'Fn::GetAtt': ['meshtestrouterF78D72DD', 'VirtualRouterName'], + }, + }, + }, + }, + }), + ); + + test.done(); + }, + }, + + 'with single virtual node provider resource': { + 'should create service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const node = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test.domain.local'), + listeners: [appmesh.VirtualNodeListener.http({ + port: 8080, + })], + }); + + new appmesh.VirtualService(stack, 'service2', { + virtualServiceName: 'test-service.domain.local', + virtualServiceProvider: appmesh.VirtualServiceProvider.virtualNode(node), + }); + + // THEN + expect(stack).to( + haveResource('AWS::AppMesh::VirtualService', { + Spec: { + Provider: { + VirtualNode: { + VirtualNodeName: { + 'Fn::GetAtt': ['meshtestnodeF93946D4', 'VirtualNodeName'], + }, + }, + }, + }, + }), + ); + + test.done(); + }, + }, + }, }; diff --git a/packages/@aws-cdk/aws-autoscaling-common/lib/interval-utils.ts b/packages/@aws-cdk/aws-autoscaling-common/lib/interval-utils.ts index 39a05beb1e70e..246bfc018507b 100644 --- a/packages/@aws-cdk/aws-autoscaling-common/lib/interval-utils.ts +++ b/packages/@aws-cdk/aws-autoscaling-common/lib/interval-utils.ts @@ -43,7 +43,7 @@ function orderAndCompleteIntervals(intervals: ScalingInterval[]): CompleteScalin intervals = intervals.map(x => ({ ...x })); // Sort by whatever number we have for each interval - intervals.sort(comparatorFromKey((x: ScalingInterval) => x.lower !== undefined ? x.lower : x.upper)); + intervals.sort(comparatorFromKey((x: ScalingInterval) => x.lower ?? x.upper)); // Propagate boundaries until no more change while (propagateBounds(intervals)) { /* Repeat */ } diff --git a/packages/@aws-cdk/aws-autoscaling-common/package.json b/packages/@aws-cdk/aws-autoscaling-common/package.json index 49bb106bd02cb..2df11c25f3c32 100644 --- a/packages/@aws-cdk/aws-autoscaling-common/package.json +++ b/packages/@aws-cdk/aws-autoscaling-common/package.json @@ -66,7 +66,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "fast-check": "^2.11.0", + "fast-check": "^2.12.1", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-autoscaling-hooktargets/lib/lambda-hook.ts b/packages/@aws-cdk/aws-autoscaling-hooktargets/lib/lambda-hook.ts index 6c3c9cd4caae2..dbe170438320e 100644 --- a/packages/@aws-cdk/aws-autoscaling-hooktargets/lib/lambda-hook.ts +++ b/packages/@aws-cdk/aws-autoscaling-hooktargets/lib/lambda-hook.ts @@ -3,9 +3,13 @@ import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; import * as subs from '@aws-cdk/aws-sns-subscriptions'; -import { Construct } from '@aws-cdk/core'; + import { TopicHook } from './topic-hook'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Use a Lambda Function as a hook target * diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 0d5dd3f147235..4b14a4ea710cb 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -957,9 +957,8 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements // desiredCapacity just reflects what the user has supplied. const desiredCapacity = props.desiredCapacity; - const minCapacity = props.minCapacity !== undefined ? props.minCapacity : 1; - const maxCapacity = props.maxCapacity !== undefined ? props.maxCapacity : - desiredCapacity !== undefined ? desiredCapacity : Math.max(minCapacity, 1); + const minCapacity = props.minCapacity ?? 1; + const maxCapacity = props.maxCapacity ?? desiredCapacity ?? Math.max(minCapacity, 1); withResolved(minCapacity, maxCapacity, (min, max) => { if (min > max) { @@ -1009,7 +1008,7 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements const { subnetIds, hasPublic } = props.vpc.selectSubnets(props.vpcSubnets); const asgProps: CfnAutoScalingGroupProps = { autoScalingGroupName: this.physicalName, - cooldown: props.cooldown !== undefined ? props.cooldown.toSeconds().toString() : undefined, + cooldown: props.cooldown?.toSeconds().toString(), minSize: Tokenization.stringifyNumber(minCapacity), maxSize: Tokenization.stringifyNumber(maxCapacity), desiredCapacity: desiredCapacity !== undefined ? Tokenization.stringifyNumber(desiredCapacity) : undefined, @@ -1509,7 +1508,7 @@ enum HealthCheckType { * Render the rolling update configuration into the appropriate object */ function renderRollingUpdateConfig(config: RollingUpdateConfiguration = {}): CfnAutoScalingRollingUpdate { - const waitOnResourceSignals = config.minSuccessfulInstancesPercent !== undefined ? true : false; + const waitOnResourceSignals = config.minSuccessfulInstancesPercent !== undefined; const pauseTime = config.pauseTime || (waitOnResourceSignals ? Duration.minutes(5) : Duration.seconds(0)); return { diff --git a/packages/@aws-cdk/aws-autoscaling/lib/lifecycle-hook-target.ts b/packages/@aws-cdk/aws-autoscaling/lib/lifecycle-hook-target.ts index b72a3245eb4d5..e15ae3ef081b5 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/lifecycle-hook-target.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/lifecycle-hook-target.ts @@ -1,6 +1,10 @@ -import { Construct } from '@aws-cdk/core'; + import { ILifecycleHook } from './lifecycle-hook'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Interface for autoscaling lifecycle hook targets */ diff --git a/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-action.ts b/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-action.ts index 9b9939d740d16..35b397e81dd04 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-action.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-action.ts @@ -1,8 +1,12 @@ -import { Construct as CoreConstruct, Duration, Lazy } from '@aws-cdk/core'; +import { Duration, Lazy } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { IAutoScalingGroup } from './auto-scaling-group'; import { CfnScalingPolicy } from './autoscaling.generated'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + /** * Properties for a scaling policy */ diff --git a/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts b/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts index a3a417bd126d5..c3b51a892c222 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts @@ -1,10 +1,14 @@ import { findAlarmThresholds, normalizeIntervals } from '@aws-cdk/aws-autoscaling-common'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import { Construct as CoreConstruct, Duration } from '@aws-cdk/core'; +import { Duration } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { IAutoScalingGroup } from './auto-scaling-group'; import { AdjustmentType, MetricAggregationType, StepScalingAction } from './step-scaling-action'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + export interface BasicStepScalingPolicyProps { /** * Metric to scale on. diff --git a/packages/@aws-cdk/aws-autoscaling/lib/target-tracking-scaling-policy.ts b/packages/@aws-cdk/aws-autoscaling/lib/target-tracking-scaling-policy.ts index 5631016066761..9a89faef3b045 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/target-tracking-scaling-policy.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/target-tracking-scaling-policy.ts @@ -1,9 +1,13 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import { Construct as CoreConstruct, Duration } from '@aws-cdk/core'; +import { Duration } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { IAutoScalingGroup } from './auto-scaling-group'; import { CfnScalingPolicy } from './autoscaling.generated'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + /** * Base interface for target tracking props * diff --git a/packages/@aws-cdk/aws-batch/lib/compute-environment.ts b/packages/@aws-cdk/aws-batch/lib/compute-environment.ts index e4b6f4ca22c33..18a2d1a446325 100644 --- a/packages/@aws-cdk/aws-batch/lib/compute-environment.ts +++ b/packages/@aws-cdk/aws-batch/lib/compute-environment.ts @@ -178,6 +178,13 @@ export interface ComputeResources { */ readonly minvCpus?: number; + /** + * The Amazon EC2 placement group to associate with your compute resources. + * + * @default - No placement group will be used. + */ + readonly placementGroup?: string; + /** * The EC2 key pair that is used for instances launched in the compute environment. * If no key is defined, then SSH access is not allowed to provisioned compute resources. @@ -365,8 +372,9 @@ export class ComputeEnvironment extends Resource implements IComputeEnvironment launchTemplate: props.computeResources.launchTemplate, maxvCpus: props.computeResources.maxvCpus || 256, minvCpus: props.computeResources.minvCpus || 0, + placementGroup: props.computeResources.placementGroup, securityGroupIds: this.buildSecurityGroupIds(props.computeResources.vpc, props.computeResources.securityGroups), - spotIamFleetRole: spotFleetRole ? spotFleetRole.roleArn : undefined, + spotIamFleetRole: spotFleetRole?.roleArn, subnets: props.computeResources.vpc.selectSubnets(props.computeResources.vpcSubnets).subnetIds, tags: props.computeResources.computeResourcesTags, type: props.computeResources.type || ComputeResourceType.ON_DEMAND, @@ -376,9 +384,8 @@ export class ComputeEnvironment extends Resource implements IComputeEnvironment const computeEnvironment = new CfnComputeEnvironment(this, 'Resource', { computeEnvironmentName: this.physicalName, computeResources, - serviceRole: props.serviceRole - ? props.serviceRole.roleArn - : new iam.Role(this, 'Resource-Service-Instance-Role', { + serviceRole: props.serviceRole?.roleArn + ?? new iam.Role(this, 'Resource-Service-Instance-Role', { managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSBatchServiceRole'), ], @@ -401,7 +408,7 @@ export class ComputeEnvironment extends Resource implements IComputeEnvironment } private isManaged(props: ComputeEnvironmentProps): boolean { - return props.managed === undefined ? true : props.managed; + return props.managed ?? true; } /** 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 f069ab3a5a080..153eb932eec37 100644 --- a/packages/@aws-cdk/aws-batch/test/compute-environment.test.ts +++ b/packages/@aws-cdk/aws-batch/test/compute-environment.test.ts @@ -184,6 +184,7 @@ describe('Batch Compute Evironment', () => { ], maxvCpus: 4, minvCpus: 1, + placementGroup: 'example-cluster-group', securityGroups: [ new ec2.SecurityGroup(stack, 'test-sg', { vpc, @@ -230,6 +231,7 @@ describe('Batch Compute Evironment', () => { ], MaxvCpus: props.computeResources.maxvCpus, MinvCpus: props.computeResources.minvCpus, + PlacementGroup: props.computeResources.placementGroup, SecurityGroupIds: [ { 'Fn::GetAtt': [ diff --git a/packages/@aws-cdk/aws-batch/test/job-definition.test.ts b/packages/@aws-cdk/aws-batch/test/job-definition.test.ts index d7d1b6fbf11b7..1658be4c8e22e 100644 --- a/packages/@aws-cdk/aws-batch/test/job-definition.test.ts +++ b/packages/@aws-cdk/aws-batch/test/job-definition.test.ts @@ -222,7 +222,7 @@ describe('Batch Job Definition', () => { // THEN expect(importedJob.jobDefinitionName).toEqual('job-def-name'); expect(importedJob.jobDefinitionArn) - .toEqual('arn:${Token[AWS.Partition.3]}:batch:${Token[AWS.Region.4]}:${Token[AWS.AccountId.0]}:job-definition/job-def-name'); + .toEqual(`arn:${cdk.Aws.PARTITION}:batch:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:job-definition/job-def-name`); }); test('can configure log configuration secrets properly', () => { 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 85da591914c1d..53c05a822acd1 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 @@ -29,7 +29,7 @@ "devDependencies": { "aws-sdk": "^2.596.0", "aws-sdk-mock": "^5.1.0", - "eslint": "^7.13.0", + "eslint": "^7.19.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", @@ -37,7 +37,7 @@ "eslint-plugin-standard": "^4.1.0", "jest": "^26.6.3", "lambda-tester": "^3.6.0", - "nock": "^13.0.5", - "ts-jest": "^26.4.4" + "nock": "^13.0.7", + "ts-jest": "^26.5.0" } } diff --git a/packages/@aws-cdk/aws-certificatemanager/test/util.test.ts b/packages/@aws-cdk/aws-certificatemanager/test/util.test.ts index 3fc833eb1d79e..2b14771c3a517 100644 --- a/packages/@aws-cdk/aws-certificatemanager/test/util.test.ts +++ b/packages/@aws-cdk/aws-certificatemanager/test/util.test.ts @@ -1,6 +1,6 @@ import '@aws-cdk/assert/jest'; import { PublicHostedZone } from '@aws-cdk/aws-route53'; -import { App, Stack } from '@aws-cdk/core'; +import { App, Aws, Stack } from '@aws-cdk/core'; import { Certificate, DnsValidatedCertificate } from '../lib'; import { apexDomain, getCertificateRegion, isDnsValidatedCertificate } from '../lib/util'; @@ -99,7 +99,7 @@ describe('getCertificateRegion', () => { domainName: 'www.example.com', }); - expect(getCertificateRegion(certificate)).toEqual('${Token[AWS.Region.4]}'); + expect(getCertificateRegion(certificate)).toEqual(Aws.REGION); }); }); diff --git a/packages/@aws-cdk/aws-cloudformation/lib/custom-resource.ts b/packages/@aws-cdk/aws-cloudformation/lib/custom-resource.ts index a36411c8504f3..9c30e6c08540c 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/custom-resource.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/custom-resource.ts @@ -8,11 +8,15 @@ import { Construct } from '@aws-cdk/core'; /** * Collection of arbitrary properties + * + * @deprecated this type has been deprecated in favor of using a key-value type directly */ export type Properties = {[key: string]: any}; /** * Configuration options for custom resource providers. + * + * @deprecated used in {@link ICustomResourceProvider} which is now deprecated */ export interface CustomResourceProviderConfig { /** @@ -37,6 +41,8 @@ export interface ICustomResourceProvider { /** * Represents a provider for an AWS CloudFormation custom resources. + * + * @deprecated use core.CustomResource instead */ export class CustomResourceProvider implements ICustomResourceProvider { /** diff --git a/packages/@aws-cdk/aws-cloudformation/lib/nested-stack.ts b/packages/@aws-cdk/aws-cloudformation/lib/nested-stack.ts index 81487ac1470b4..c72d94598ecfc 100644 --- a/packages/@aws-cdk/aws-cloudformation/lib/nested-stack.ts +++ b/packages/@aws-cdk/aws-cloudformation/lib/nested-stack.ts @@ -8,7 +8,7 @@ import { Construct } from '@aws-cdk/core'; /** * Initialization props for the `NestedStack` construct. * - * @experimental + * @deprecated use core.NestedStackProps instead */ export interface NestedStackProps { /** @@ -64,14 +64,14 @@ export interface NestedStackProps { * nested stack will automatically be translated to stack parameters and * outputs. * - * @experimental + * @deprecated use core.NestedStack instead */ export class NestedStack extends core.NestedStack { constructor(scope: Construct, id: string, props: NestedStackProps = { }) { super(scope, id, { parameters: props.parameters, timeout: props.timeout, - notificationArns: props.notifications ? props.notifications.map(n => n.topicArn) : undefined, + notificationArns: props.notifications?.map(n => n.topicArn), }); } } diff --git a/packages/@aws-cdk/aws-cloudformation/package.json b/packages/@aws-cdk/aws-cloudformation/package.json index a2afd8f325bd6..a3acf41a02170 100644 --- a/packages/@aws-cdk/aws-cloudformation/package.json +++ b/packages/@aws-cdk/aws-cloudformation/package.json @@ -73,7 +73,7 @@ "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", - "@types/aws-lambda": "^8.10.64", + "@types/aws-lambda": "^8.10.71", "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts index 41029448ec1dc..1743444c1ad57 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts @@ -7,7 +7,11 @@ * - GetAtt.Attribute1: "foo" * - GetAtt.Attribute2: 1234 */ -import { App, CfnOutput, Construct, CustomResource, CustomResourceProvider, CustomResourceProviderRuntime, Stack, Token } from '@aws-cdk/core'; +import { App, CfnOutput, CustomResource, CustomResourceProvider, CustomResourceProviderRuntime, Stack, Token } from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; /* eslint-disable cdk/no-core-construct */ diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stack.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stack.ts index 650609c0e6d40..b6357115f7667 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stack.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stack.ts @@ -2,9 +2,13 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; import * as sns_subscriptions from '@aws-cdk/aws-sns-subscriptions'; import * as sqs from '@aws-cdk/aws-sqs'; -import { App, CfnParameter, Construct, Stack } from '@aws-cdk/core'; +import { App, CfnParameter, Stack } from '@aws-cdk/core'; import * as cfn from '../lib'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /* eslint-disable cdk/no-core-construct */ interface MyNestedStackProps { @@ -42,7 +46,7 @@ class MyNestedStack extends cfn.NestedStack { code: lambda.Code.inline('console.error("hi")'), handler: 'index.handler', environment: { - TOPIC_ARN: props.siblingTopic ? props.siblingTopic.topicArn : '', + TOPIC_ARN: props.siblingTopic?.topicArn ?? '', QUEUE_URL: props.subscriber.queueUrl, // nested stack references a resource in the parent }, }); diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-assets.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-assets.ts index 4a6599c289928..ad7ed4e85d294 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-assets.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-assets.ts @@ -1,9 +1,13 @@ /// !cdk-integ pragma:ignore-assets import * as path from 'path'; import * as lambda from '@aws-cdk/aws-lambda'; -import { App, Construct, Stack } from '@aws-cdk/core'; +import { App, Stack } from '@aws-cdk/core'; import * as cfn from '../lib'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /* eslint-disable cdk/no-core-construct */ class NestedStack extends cfn.NestedStack { 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 109b151138b19..67f11d0f31878 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,8 +1,12 @@ /// !cdk-integ pragma:ignore-assets import * as sns from '@aws-cdk/aws-sns'; -import { App, Construct, Stack } from '@aws-cdk/core'; +import { App, Stack } from '@aws-cdk/core'; import * as cfn from '../lib'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /* eslint-disable cdk/no-core-construct */ class YourNestedStack extends cfn.NestedStack { diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs1.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs1.ts index b6ca8b087f7ad..1240d8d39e5b3 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs1.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs1.ts @@ -5,9 +5,13 @@ /* eslint-disable cdk/no-core-construct */ import * as sns from '@aws-cdk/aws-sns'; -import { App, Construct, Stack } from '@aws-cdk/core'; +import { App, Stack } from '@aws-cdk/core'; import * as cfn from '../lib'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + class ConsumerNestedStack extends cfn.NestedStack { constructor(scope: Construct, id: string, topic: sns.Topic) { super(scope, id); diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs2.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs2.ts index 5b884b209786b..712de493fa9a3 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs2.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs2.ts @@ -1,8 +1,12 @@ /// !cdk-integ * import * as sns from '@aws-cdk/aws-sns'; -import { App, Construct, Fn, Stack } from '@aws-cdk/core'; +import { App, Fn, Stack } from '@aws-cdk/core'; import * as cfn from '../lib'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + // non-nested non-parent stack consumes a resource from a nested stack /* eslint-disable cdk/no-core-construct */ diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs3.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs3.ts index ab1f51be91330..f44eb1b1f731a 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs3.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-refs3.ts @@ -1,8 +1,12 @@ /// !cdk-integ * import * as sns from '@aws-cdk/aws-sns'; -import { App, Construct, Fn, Stack } from '@aws-cdk/core'; +import { App, Fn, Stack } from '@aws-cdk/core'; import * as cfn from '../lib'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + // references between siblings /* eslint-disable cdk/no-core-construct */ 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 056849c1dc12b..64656faf0fca3 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts @@ -3,10 +3,14 @@ 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, LegacyStackSynthesizer, Names, Stack } from '@aws-cdk/core'; +import { App, CfnParameter, CfnResource, ContextProvider, LegacyStackSynthesizer, Names, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import { NestedStack } from '../lib/nested-stack'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /* eslint-disable cdk/no-core-construct */ /* eslint-disable max-len */ diff --git a/packages/@aws-cdk/aws-cloudfront-origins/README.md b/packages/@aws-cdk/aws-cloudfront-origins/README.md index c7741e37937e3..05d12bc1ae6f4 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/README.md +++ b/packages/@aws-cdk/aws-cloudfront-origins/README.md @@ -29,7 +29,7 @@ new cloudfront.Distribution(this, 'myDist', { The above will treat the bucket differently based on if `IBucket.isWebsite` is set or not. If the bucket is configured as a website, the bucket is treated as an HTTP origin, and the built-in S3 redirects and error pages can be used. Otherwise, the bucket is handled as a bucket origin and -CloudFront's redirect and error handling will be used. In the latter case, the Origin wil create an origin access identity and grant it access to the +CloudFront's redirect and error handling will be used. In the latter case, the Origin will create an origin access identity and grant it access to the underlying bucket. This can be used in conjunction with a bucket that is not public to require that your users access your content using CloudFront URLs and not S3 URLs directly. Alternatively, a custom origin access identity can be passed to the S3 origin in the properties. diff --git a/packages/@aws-cdk/aws-cloudfront-origins/package.json b/packages/@aws-cdk/aws-cloudfront-origins/package.json index c6c5e867e0471..aa361d322caa1 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/package.json +++ b/packages/@aws-cdk/aws-cloudfront-origins/package.json @@ -71,7 +71,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", - "aws-sdk": "^2.830.0", + "aws-sdk": "^2.842.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/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index b364cedd61b2a..34b0922913c4c 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -51,7 +51,7 @@ new cloudfront.Distribution(this, 'myDist', { The above will treat the bucket differently based on if `IBucket.isWebsite` is set or not. If the bucket is configured as a website, the bucket is treated as an HTTP origin, and the built-in S3 redirects and error pages can be used. Otherwise, the bucket is handled as a bucket origin and -CloudFront's redirect and error handling will be used. In the latter case, the Origin wil create an origin access identity and grant it access to the +CloudFront's redirect and error handling will be used. In the latter case, the Origin will create an origin access identity and grant it access to the underlying bucket. This can be used in conjunction with a bucket that is not public to require that your users access your content using CloudFront URLs and not S3 URLs directly. @@ -239,6 +239,34 @@ new cloudfront.Distribution(this, 'myDistCustomPolicy', { }); ``` +### Validating signed URLs or signed cookies with Trusted Key Groups + +CloudFront Distribution now supports validating signed URLs or signed cookies using key groups. When a cache behavior contains trusted key groups, CloudFront requires signed URLs or signed cookies for all requests that match the cache behavior. + +Example: + +```ts +// public key in PEM format +const pubKey = new PublicKey(stack, 'MyPubKey', { + encodedKey: publicKey, +}); + +const keyGroup = new KeyGroup(stack, 'MyKeyGroup', { + items: [ + pubKey, + ], +}); + +new cloudfront.Distribution(stack, 'Dist', { + defaultBehavior: { + origin: new origins.HttpOrigin('www.example.com'), + trustedKeyGroups: [ + keyGroup, + ], + }, +}); +``` + ### Lambda@Edge Lambda@Edge is an extension of AWS Lambda, a compute service that lets you execute functions that customize the content that CloudFront delivers. @@ -274,7 +302,7 @@ new cloudfront.Distribution(this, 'myDist', { > **Note:** Lambda@Edge functions must be created in the `us-east-1` region, regardless of the region of the CloudFront distribution and stack. > To make it easier to request functions for Lambda@Edge, the `EdgeFunction` construct can be used. > The `EdgeFunction` construct will automatically request a function in `us-east-1`, regardless of the region of the current stack. -> `EdgeFunction` has the same interface as `Function` and can be created and used interchangably. +> `EdgeFunction` has the same interface as `Function` and can be created and used interchangeably. > Please note that using `EdgeFunction` requires that the `us-east-1` region has been bootstrapped. > See https://docs.aws.amazon.com/cdk/latest/guide/bootstrapping.html for more about bootstrapping regions. @@ -289,7 +317,7 @@ const myFunc = new lambda.Function(this, 'MyFunction', { ``` If the stack is not in `us-east-1`, and you need references from different applications on the same account, -you can also set a specific stack ID for each Lamba@Edge. +you can also set a specific stack ID for each Lambda@Edge. ```ts const myFunc1 = new cloudfront.experimental.EdgeFunction(this, 'MyFunction1', { @@ -373,10 +401,10 @@ new cloudfront.Distribution(this, 'myDist', { // You can optionally log to a specific bucket, configure whether cookies are logged, and give the log files a prefix. new cloudfront.Distribution(this, 'myDist', { defaultBehavior: { origin: new origins.HttpOrigin('www.example.com') }, - enableLogging: true, // Optional, this is implied if loggingBucket is specified - loggingBucket: new s3.Bucket(this, 'LoggingBucket'), - loggingFilePrefix: 'distribution-access-logs/', - loggingIncludesCookies: true, + enableLogging: true, // Optional, this is implied if logBucket is specified + logBucket: new s3.Bucket(this, 'LogBucket'), + logFilePrefix: 'distribution-access-logs/', + logIncludesCookies: true, }); ``` @@ -427,7 +455,7 @@ You can customize the default certificate aliases. This is intended to be used i Example: -[create a distrubution with an default certificiate example](test/example.default-cert-alias.lit.ts) +[create a distribution with an default certificate example](test/example.default-cert-alias.lit.ts) #### ACM certificate @@ -438,7 +466,7 @@ For more information, see [the aws-certificatemanager module documentation](http Example: -[create a distrubution with an acm certificate example](test/example.acm-cert-alias.lit.ts) +[create a distribution with an acm certificate example](test/example.acm-cert-alias.lit.ts) #### IAM certificate @@ -448,7 +476,43 @@ See [Importing an SSL/TLS Certificate](https://docs.aws.amazon.com/AmazonCloudFr Example: -[create a distrubution with an iam certificate example](test/example.iam-cert-alias.lit.ts) +[create a distribution with an iam certificate example](test/example.iam-cert-alias.lit.ts) + +### Trusted Key Groups + +CloudFront Web Distributions supports validating signed URLs or signed cookies using key groups. When a cache behavior contains trusted key groups, CloudFront requires signed URLs or signed cookies for all requests that match the cache behavior. + +Example: + +```ts +const pubKey = new PublicKey(stack, 'MyPubKey', { + encodedKey: publicKey, +}); + +const keyGroup = new KeyGroup(stack, 'MyKeyGroup', { + items: [ + pubKey, + ], +}); + +new CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', { + originConfigs: [ + { + s3OriginSource: { + s3BucketSource: sourceBucket, + }, + behaviors: [ + { + isDefaultBehavior: true, + trustedKeyGroups: [ + keyGroup, + ], + }, + ], + }, + ], +}); +``` ### Restrictions @@ -505,7 +569,7 @@ new CloudFrontWebDistribution(stack, 'ADistribution', { }, failoverS3OriginSource: { s3BucketSource: s3.Bucket.fromBucketName(stack, 'aBucketFallback', 'myoriginbucketfallback'), - originPath: '/somwhere', + originPath: '/somewhere', originHeaders: { 'myHeader2': '21', }, @@ -520,3 +584,40 @@ new CloudFrontWebDistribution(stack, 'ADistribution', { ], }); ``` + +## KeyGroup & PublicKey API + +Now you can create a key group to use with CloudFront signed URLs and signed cookies. You can add public keys to use with CloudFront features such as signed URLs, signed cookies, and field-level encryption. + +The following example command uses OpenSSL to generate an RSA key pair with a length of 2048 bits and save to the file named `private_key.pem`. + +```bash +openssl genrsa -out private_key.pem 2048 +``` + +The resulting file contains both the public and the private key. The following example command extracts the public key from the file named `private_key.pem` and stores it in `public_key.pem`. + +```bash +openssl rsa -pubout -in private_key.pem -out public_key.pem +``` + +Note: Don't forget to copy/paste the contents of `public_key.pem` file including `-----BEGIN PUBLIC KEY-----` and `-----END PUBLIC KEY-----` lines into `encodedKey` parameter when creating a `PublicKey`. + +Example: + +```ts + new cloudfront.KeyGroup(stack, 'MyKeyGroup', { + items: [ + new cloudfront.PublicKey(stack, 'MyPublicKey', { + encodedKey: '...', // contents of public_key.pem file + // comment: 'Key is expiring on ...', + }), + ], + // comment: 'Key group containing public keys ...', + }); +``` + +See: + +* https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html +* https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 05534862026ae..02e3d092295f3 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -6,6 +6,7 @@ import { Construct } from 'constructs'; import { ICachePolicy } from './cache-policy'; import { CfnDistribution } from './cloudfront.generated'; import { GeoRestriction } from './geo-restriction'; +import { IKeyGroup } from './key-group'; import { IOrigin, OriginBindConfig, OriginBindOptions } from './origin'; import { IOriginRequestPolicy } from './origin-request-policy'; import { CacheBehavior } from './private/cache-behavior'; @@ -706,6 +707,14 @@ export interface AddBehaviorOptions { * @see https://aws.amazon.com/lambda/edge */ readonly edgeLambdas?: EdgeLambda[]; + + /** + * A list of Key Groups that CloudFront can use to validate signed URLs or signed cookies. + * + * @default - no KeyGroups are associated with cache behavior + * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html + */ + readonly trustedKeyGroups?: IKeyGroup[]; } /** diff --git a/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts b/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts index e1d7a5dd9c960..f97b7e176d874 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts @@ -214,7 +214,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion { throw new Error('stacks which use EdgeFunctions must have an explicitly set region'); } - const edgeStackId = stackId ?? `edge-lambda-stack-${region}`; + const edgeStackId = stackId ?? `edge-lambda-stack-${this.stack.node.addr}`; let edgeStack = stage.node.tryFindChild(edgeStackId) as Stack; if (!edgeStack) { edgeStack = new Stack(stage, edgeStackId, { diff --git a/packages/@aws-cdk/aws-cloudfront/lib/index.ts b/packages/@aws-cdk/aws-cloudfront/lib/index.ts index b0bd550231be3..7de2aa62b4412 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/index.ts @@ -1,10 +1,12 @@ export * from './cache-policy'; export * from './distribution'; export * from './geo-restriction'; +export * from './key-group'; export * from './origin'; -export * from './origin_access_identity'; +export * from './origin-access-identity'; export * from './origin-request-policy'; -export * from './web_distribution'; +export * from './public-key'; +export * from './web-distribution'; export * as experimental from './experimental'; diff --git a/packages/@aws-cdk/aws-cloudfront/lib/key-group.ts b/packages/@aws-cdk/aws-cloudfront/lib/key-group.ts new file mode 100644 index 0000000000000..aea7bf451f305 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/lib/key-group.ts @@ -0,0 +1,75 @@ +import { IResource, Names, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnKeyGroup } from './cloudfront.generated'; +import { IPublicKey } from './public-key'; + +/** + * Represents a Key Group + */ +export interface IKeyGroup extends IResource { + /** + * The ID of the key group. + * @attribute + */ + readonly keyGroupId: string; +} + +/** + * Properties for creating a Public Key + */ +export interface KeyGroupProps { + /** + * A name to identify the key group. + * @default - generated from the `id` + */ + readonly keyGroupName?: string; + + /** + * A comment to describe the key group. + * @default - no comment + */ + readonly comment?: string; + + /** + * A list of public keys to add to the key group. + */ + readonly items: IPublicKey[]; +} + +/** + * A Key Group configuration + * + * @resource AWS::CloudFront::KeyGroup + */ +export class KeyGroup extends Resource implements IKeyGroup { + + /** Imports a Key Group from its id. */ + public static fromKeyGroupId(scope: Construct, id: string, keyGroupId: string): IKeyGroup { + return new class extends Resource implements IKeyGroup { + public readonly keyGroupId = keyGroupId; + }(scope, id); + } + public readonly keyGroupId: string; + + constructor(scope: Construct, id: string, props: KeyGroupProps) { + super(scope, id); + + const resource = new CfnKeyGroup(this, 'Resource', { + keyGroupConfig: { + name: props.keyGroupName ?? this.generateName(), + comment: props.comment, + items: props.items.map(key => key.publicKeyId), + }, + }); + + this.keyGroupId = resource.ref; + } + + private generateName(): string { + const name = Names.uniqueId(this); + if (name.length > 80) { + return name.substring(0, 40) + name.substring(name.length - 40); + } + return name; + } +} diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin_access_identity.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin-access-identity.ts similarity index 100% rename from packages/@aws-cdk/aws-cloudfront/lib/origin_access_identity.ts rename to packages/@aws-cdk/aws-cloudfront/lib/origin-access-identity.ts diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts index 0722dff17099d..6f5a42e407e01 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -1,6 +1,10 @@ -import { Construct, Duration, Token } from '@aws-cdk/core'; +import { Duration, Token } from '@aws-cdk/core'; import { CfnDistribution } from './cloudfront.generated'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * The failover configuration used for Origin Groups, * returned in {@link OriginBindConfig.failoverConfig}. diff --git a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts index 4b700e166cecc..d804dd8465750 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts @@ -50,13 +50,12 @@ export class CacheBehavior { originRequestPolicyId: this.props.originRequestPolicy?.originRequestPolicyId, smoothStreaming: this.props.smoothStreaming, viewerProtocolPolicy: this.props.viewerProtocolPolicy ?? ViewerProtocolPolicy.ALLOW_ALL, - lambdaFunctionAssociations: this.props.edgeLambdas - ? this.props.edgeLambdas.map(edgeLambda => ({ - lambdaFunctionArn: edgeLambda.functionVersion.edgeArn, - eventType: edgeLambda.eventType.toString(), - includeBody: edgeLambda.includeBody, - })) - : undefined, + lambdaFunctionAssociations: this.props.edgeLambdas?.map(edgeLambda => ({ + lambdaFunctionArn: edgeLambda.functionVersion.edgeArn, + eventType: edgeLambda.eventType.toString(), + includeBody: edgeLambda.includeBody, + })), + trustedKeyGroups: this.props.trustedKeyGroups?.map(keyGroup => keyGroup.keyGroupId), }; } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/public-key.ts b/packages/@aws-cdk/aws-cloudfront/lib/public-key.ts new file mode 100644 index 0000000000000..e2c2b6e044cdb --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/lib/public-key.ts @@ -0,0 +1,83 @@ +import { IResource, Names, Resource, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnPublicKey } from './cloudfront.generated'; + +/** + * Represents a Public Key + */ +export interface IPublicKey extends IResource { + /** + * The ID of the key group. + * @attribute + */ + readonly publicKeyId: string; +} + +/** + * Properties for creating a Public Key + */ +export interface PublicKeyProps { + /** + * A name to identify the public key. + * @default - generated from the `id` + */ + readonly publicKeyName?: string; + + /** + * A comment to describe the public key. + * @default - no comment + */ + readonly comment?: string; + + /** + * The public key that you can use with signed URLs and signed cookies, or with field-level encryption. + * The `encodedKey` parameter must include `-----BEGIN PUBLIC KEY-----` and `-----END PUBLIC KEY-----` lines. + * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html + * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/field-level-encryption.html + */ + readonly encodedKey: string; +} + +/** + * A Public Key Configuration + * + * @resource AWS::CloudFront::PublicKey + */ +export class PublicKey extends Resource implements IPublicKey { + + /** Imports a Public Key from its id. */ + public static fromPublicKeyId(scope: Construct, id: string, publicKeyId: string): IPublicKey { + return new class extends Resource implements IPublicKey { + public readonly publicKeyId = publicKeyId; + }(scope, id); + } + + public readonly publicKeyId: string; + + constructor(scope: Construct, id: string, props: PublicKeyProps) { + super(scope, id); + + if (!Token.isUnresolved(props.encodedKey) && !/^-----BEGIN PUBLIC KEY-----/.test(props.encodedKey)) { + throw new Error(`Public key must be in PEM format (with the BEGIN/END PUBLIC KEY lines); got ${props.encodedKey}`); + } + + const resource = new CfnPublicKey(this, 'Resource', { + publicKeyConfig: { + name: props.publicKeyName ?? this.generateName(), + callerReference: this.node.addr, + encodedKey: props.encodedKey, + comment: props.comment, + }, + }); + + this.publicKeyId = resource.ref; + } + + private generateName(): string { + const name = Names.uniqueId(this); + if (name.length > 80) { + return name.substring(0, 40) + name.substring(name.length - 40); + } + return name; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts similarity index 98% rename from packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts rename to packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts index dad54c3b1488a..f191813dada2b 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web-distribution.ts @@ -7,7 +7,8 @@ import { Construct } from 'constructs'; import { CfnDistribution } from './cloudfront.generated'; import { HttpVersion, IDistribution, LambdaEdgeEventType, OriginProtocolPolicy, PriceClass, ViewerProtocolPolicy, SSLMethod, SecurityPolicyProtocol } from './distribution'; import { GeoRestriction } from './geo-restriction'; -import { IOriginAccessIdentity } from './origin_access_identity'; +import { IKeyGroup } from './key-group'; +import { IOriginAccessIdentity } from './origin-access-identity'; /** * HTTP status code to failover to second origin @@ -347,9 +348,18 @@ export interface Behavior { * The signers are the account IDs that are allowed to sign cookies/presigned URLs for this distribution. * * If you pass a non empty value, all requests for this behavior must be signed (no public access will be allowed) + * @deprecated - We recommend using trustedKeyGroups instead of trustedSigners. */ readonly trustedSigners?: string[]; + /** + * A list of Key Groups that CloudFront can use to validate signed URLs or signed cookies. + * + * @default - no KeyGroups are associated with cache behavior + * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html + */ + readonly trustedKeyGroups?: IKeyGroup[]; + /** * * The default amount of time CloudFront will cache an object. @@ -761,10 +771,10 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu let distributionConfig: CfnDistribution.DistributionConfigProperty = { comment: props.comment, enabled: true, - defaultRootObject: props.defaultRootObject !== undefined ? props.defaultRootObject : 'index.html', + defaultRootObject: props.defaultRootObject ?? 'index.html', httpVersion: props.httpVersion || HttpVersion.HTTP2, priceClass: props.priceClass || PriceClass.PRICE_CLASS_100, - ipv6Enabled: (props.enableIpV6 !== undefined) ? props.enableIpV6 : true, + ipv6Enabled: props.enableIpV6 ?? true, // eslint-disable-next-line max-len customErrorResponses: props.errorConfigurations, // TODO: validation : https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-customerrorresponse.html#cfn-cloudfront-distribution-customerrorresponse-errorcachingminttl webAclId: props.webACLId, @@ -932,6 +942,7 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu forwardedValues: input.forwardedValues || { queryString: false, cookies: { forward: 'none' } }, maxTtl: input.maxTtl && input.maxTtl.toSeconds(), minTtl: input.minTtl && input.minTtl.toSeconds(), + trustedKeyGroups: input.trustedKeyGroups?.map(key => key.keyGroupId), trustedSigners: input.trustedSigners, targetOriginId: input.targetOriginId, viewerProtocolPolicy: protoPolicy || ViewerProtocolPolicy.REDIRECT_TO_HTTPS, diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 651d22e95e668..a519240ea2f31 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -72,7 +72,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "aws-sdk": "^2.830.0", + "aws-sdk": "^2.842.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -153,7 +153,9 @@ "resource-attribute:@aws-cdk/aws-cloudfront.CachePolicy.cachePolicyLastModifiedTime", "construct-interface-extends-iconstruct:@aws-cdk/aws-cloudfront.IOriginRequestPolicy", "resource-interface-extends-resource:@aws-cdk/aws-cloudfront.IOriginRequestPolicy", - "resource-attribute:@aws-cdk/aws-cloudfront.OriginRequestPolicy.originRequestPolicyLastModifiedTime" + "resource-attribute:@aws-cdk/aws-cloudfront.OriginRequestPolicy.originRequestPolicyLastModifiedTime", + "resource-attribute:@aws-cdk/aws-cloudfront.KeyGroup.keyGroupLastModifiedTime", + "resource-attribute:@aws-cdk/aws-cloudfront.PublicKey.publicKeyCreatedTime" ] }, "awscdkio": { diff --git a/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts b/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts index 372caea5c3fb6..4154f79cabff2 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts @@ -250,10 +250,10 @@ function defaultEdgeFunctionProps(stackId?: string) { code: lambda.Code.fromInline('foo'), handler: 'index.handler', runtime: lambda.Runtime.NODEJS_12_X, - stackId: stackId ?? 'edge-lambda-stack-testregion', + stackId: stackId, }; } -function getFnStack(region: string = 'testregion'): cdk.Stack { - return app.node.findChild(`edge-lambda-stack-${region}`) as cdk.Stack; +function getFnStack(): cdk.Stack { + return app.node.findChild(`edge-lambda-stack-${stack.node.addr}`) as cdk.Stack; } diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-key-group.expected.json b/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-key-group.expected.json new file mode 100644 index 0000000000000..45191bad86cff --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-key-group.expected.json @@ -0,0 +1,27 @@ +{ + "Resources": { + "AwesomePublicKeyED3E7F55": { + "Type": "AWS::CloudFront::PublicKey", + "Properties": { + "PublicKeyConfig": { + "CallerReference": "c88e460888c5762c9c47ac0cdc669370d787fb2d9f", + "EncodedKey": "-----BEGIN PUBLIC KEY-----\n MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS\n JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa\n dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj\n 6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e\n 0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD\n /3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx\n NQIDAQAB\n -----END PUBLIC KEY-----\n ", + "Name": "awscdkcloudfrontcustomAwesomePublicKey0E83393B" + } + } + }, + "AwesomeKeyGroup3EF8348B": { + "Type": "AWS::CloudFront::KeyGroup", + "Properties": { + "KeyGroupConfig": { + "Items": [ + { + "Ref": "AwesomePublicKeyED3E7F55" + } + ], + "Name": "awscdkcloudfrontcustomAwesomeKeyGroup73FD4DCA" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-key-group.ts b/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-key-group.ts new file mode 100644 index 0000000000000..7bfdbbe645446 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.cloudfront-key-group.ts @@ -0,0 +1,25 @@ +import * as cdk from '@aws-cdk/core'; +import * as cloudfront from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-cloudfront-custom'); + +new cloudfront.KeyGroup(stack, 'AwesomeKeyGroup', { + items: [ + new cloudfront.PublicKey(stack, 'AwesomePublicKey', { + encodedKey: `-----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS + JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa + dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj + 6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e + 0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD + /3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx + NQIDAQAB + -----END PUBLIC KEY----- + `, + }), + ], +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-key-group.expected.json b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-key-group.expected.json new file mode 100644 index 0000000000000..afae558dee709 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-key-group.expected.json @@ -0,0 +1,57 @@ +{ + "Resources": { + "MyPublicKey78071F3D": { + "Type": "AWS::CloudFront::PublicKey", + "Properties": { + "PublicKeyConfig": { + "CallerReference": "c8752fac3fe06fc93f3fbd12d7e0282d8967409e4d", + "EncodedKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS\nJAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa\ndlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj\n6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e\n0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD\n/3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx\nNQIDAQAB\n-----END PUBLIC KEY-----", + "Name": "integdistributionkeygroupMyPublicKeyC0F3B115" + } + } + }, + "MyKeyGroupAF22FD35": { + "Type": "AWS::CloudFront::KeyGroup", + "Properties": { + "KeyGroupConfig": { + "Items": [ + { + "Ref": "MyPublicKey78071F3D" + } + ], + "Name": "integdistributionkeygroupMyKeyGroupF179E01A" + } + } + }, + "DistB3B78991": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "DefaultCacheBehavior": { + "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", + "Compress": true, + "TargetOriginId": "integdistributionkeygroupDistOrigin1B9677703", + "TrustedKeyGroups": [ + { + "Ref": "MyKeyGroupAF22FD35" + } + ], + "ViewerProtocolPolicy": "allow-all" + }, + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Origins": [ + { + "CustomOriginConfig": { + "OriginProtocolPolicy": "https-only" + }, + "DomainName": "www.example.com", + "Id": "integdistributionkeygroupDistOrigin1B9677703" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-key-group.ts b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-key-group.ts new file mode 100644 index 0000000000000..e0a124b26f904 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-key-group.ts @@ -0,0 +1,32 @@ +import * as cdk from '@aws-cdk/core'; +import * as cloudfront from '../lib'; +import { TestOrigin } from './test-origin'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'integ-distribution-key-group'); +const publicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS +JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa +dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj +6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e +0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD +/3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx +NQIDAQAB +-----END PUBLIC KEY-----`; + +new cloudfront.Distribution(stack, 'Dist', { + defaultBehavior: { + origin: new TestOrigin('www.example.com'), + trustedKeyGroups: [ + new cloudfront.KeyGroup(stack, 'MyKeyGroup', { + items: [ + new cloudfront.PublicKey(stack, 'MyPublicKey', { + encodedKey: publicKey, + }), + ], + }), + ], + }, +}); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts b/packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts new file mode 100644 index 0000000000000..f5a0ae43c0855 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts @@ -0,0 +1,187 @@ +import '@aws-cdk/assert/jest'; +import { expect as expectStack } from '@aws-cdk/assert'; +import { App, Stack } from '@aws-cdk/core'; +import { KeyGroup, PublicKey } from '../lib'; + +const publicKey1 = `-----BEGIN PUBLIC KEY----- +FIRST_KEYgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS +JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa +dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj +6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e +0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD +/3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx +NQIDAQAB +-----END PUBLIC KEY-----`; + +const publicKey2 = `-----BEGIN PUBLIC KEY----- +SECOND_KEYkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS +JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa +dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj +6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e +0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD +/3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx +NQIDAQAB +-----END PUBLIC KEY-----`; + +describe('KeyGroup', () => { + let app: App; + let stack: Stack; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '123456789012', region: 'testregion' }, + }); + }); + + test('import existing key group by id', () => { + const keyGroupId = '344f6fe5-7ce5-4df0-a470-3f14177c549c'; + const keyGroup = KeyGroup.fromKeyGroupId(stack, 'MyKeyGroup', keyGroupId); + expect(keyGroup.keyGroupId).toEqual(keyGroupId); + }); + + test('minimal example', () => { + new KeyGroup(stack, 'MyKeyGroup', { + items: [ + new PublicKey(stack, 'MyPublicKey', { + encodedKey: publicKey1, + }), + ], + }); + + expectStack(stack).toMatch({ + Resources: { + MyPublicKey78071F3D: { + Type: 'AWS::CloudFront::PublicKey', + Properties: { + PublicKeyConfig: { + CallerReference: 'c872d91ae0d2943aad25d4b31f1304d0a62c658ace', + EncodedKey: publicKey1, + Name: 'StackMyPublicKey36EDA6AB', + }, + }, + }, + MyKeyGroupAF22FD35: { + Type: 'AWS::CloudFront::KeyGroup', + Properties: { + KeyGroupConfig: { + Items: [ + { + Ref: 'MyPublicKey78071F3D', + }, + ], + Name: 'StackMyKeyGroupC9D82374', + }, + }, + }, + }, + }); + }); + + test('maximum example', () => { + new KeyGroup(stack, 'MyKeyGroup', { + keyGroupName: 'AcmeKeyGroup', + comment: 'Key group created on 1/1/1984', + items: [ + new PublicKey(stack, 'MyPublicKey', { + publicKeyName: 'pub-key', + encodedKey: publicKey1, + comment: 'Key expiring on 1/1/1984', + }), + ], + }); + + expectStack(stack).toMatch({ + Resources: { + MyPublicKey78071F3D: { + Type: 'AWS::CloudFront::PublicKey', + Properties: { + PublicKeyConfig: { + CallerReference: 'c872d91ae0d2943aad25d4b31f1304d0a62c658ace', + EncodedKey: publicKey1, + Name: 'pub-key', + Comment: 'Key expiring on 1/1/1984', + }, + }, + }, + MyKeyGroupAF22FD35: { + Type: 'AWS::CloudFront::KeyGroup', + Properties: { + KeyGroupConfig: { + Items: [ + { + Ref: 'MyPublicKey78071F3D', + }, + ], + Name: 'AcmeKeyGroup', + Comment: 'Key group created on 1/1/1984', + }, + }, + }, + }, + }); + }); + + test('multiple keys example', () => { + new KeyGroup(stack, 'MyKeyGroup', { + keyGroupName: 'AcmeKeyGroup', + comment: 'Key group created on 1/1/1984', + items: [ + new PublicKey(stack, 'BingoKey', { + publicKeyName: 'Bingo-Key', + encodedKey: publicKey1, + comment: 'Key expiring on 1/1/1984', + }), + new PublicKey(stack, 'RollyKey', { + publicKeyName: 'Rolly-Key', + encodedKey: publicKey2, + comment: 'Key expiring on 1/1/1984', + }), + ], + }); + + expectStack(stack).toMatch({ + Resources: { + BingoKeyCBEC786C: { + Type: 'AWS::CloudFront::PublicKey', + Properties: { + PublicKeyConfig: { + CallerReference: 'c847cb3dc23f619c0a1e400a44afaf1060d27a1d1a', + EncodedKey: publicKey1, + Name: 'Bingo-Key', + Comment: 'Key expiring on 1/1/1984', + }, + }, + }, + RollyKey83F8BC5B: { + Type: 'AWS::CloudFront::PublicKey', + Properties: { + PublicKeyConfig: { + CallerReference: 'c83a16945c386bf6cd88a3aaa1aa603eeb4b6c6c57', + EncodedKey: publicKey2, + Name: 'Rolly-Key', + Comment: 'Key expiring on 1/1/1984', + }, + }, + }, + MyKeyGroupAF22FD35: { + Type: 'AWS::CloudFront::KeyGroup', + Properties: { + KeyGroupConfig: { + Items: [ + { + Ref: 'BingoKeyCBEC786C', + }, + { + Ref: 'RollyKey83F8BC5B', + }, + ], + Name: 'AcmeKeyGroup', + Comment: 'Key group created on 1/1/1984', + }, + }, + }, + }, + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts index a1e315fdcdd96..c9500b23eaddf 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/private/cache-behavior.test.ts @@ -1,11 +1,20 @@ import '@aws-cdk/assert/jest'; import * as lambda from '@aws-cdk/aws-lambda'; import { App, Stack } from '@aws-cdk/core'; -import { AllowedMethods, CachedMethods, CachePolicy, LambdaEdgeEventType, OriginRequestPolicy, ViewerProtocolPolicy } from '../../lib'; +import { AllowedMethods, CachedMethods, CachePolicy, KeyGroup, LambdaEdgeEventType, OriginRequestPolicy, PublicKey, ViewerProtocolPolicy } from '../../lib'; import { CacheBehavior } from '../../lib/private/cache-behavior'; let app: App; let stack: Stack; +const publicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS +JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa +dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj +6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e +0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD +/3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx +NQIDAQAB +-----END PUBLIC KEY-----`; beforeEach(() => { app = new App(); @@ -30,6 +39,15 @@ test('renders the minimum template with an origin and path specified', () => { test('renders with all properties specified', () => { const fnVersion = lambda.Version.fromVersionArn(stack, 'Version', 'arn:aws:lambda:testregion:111111111111:function:myTestFun:v1'); + const pubKey = new PublicKey(stack, 'MyPublicKey', { + encodedKey: publicKey, + }); + const keyGroup = new KeyGroup(stack, 'MyKeyGroup', { + items: [ + pubKey, + ], + }); + const behavior = new CacheBehavior('origin_id', { pathPattern: '*', allowedMethods: AllowedMethods.ALLOW_ALL, @@ -44,6 +62,7 @@ test('renders with all properties specified', () => { includeBody: true, functionVersion: fnVersion, }], + trustedKeyGroups: [keyGroup], }); expect(behavior._renderBehavior()).toEqual({ @@ -61,6 +80,9 @@ test('renders with all properties specified', () => { eventType: 'origin-request', includeBody: true, }], + trustedKeyGroups: [ + keyGroup.keyGroupId, + ], }); }); diff --git a/packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts b/packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts new file mode 100644 index 0000000000000..934b1a9dc8107 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts @@ -0,0 +1,85 @@ +import '@aws-cdk/assert/jest'; +import { expect as expectStack } from '@aws-cdk/assert'; +import { App, Stack } from '@aws-cdk/core'; +import { PublicKey } from '../lib'; + +const publicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS +JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa +dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj +6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e +0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD +/3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx +NQIDAQAB +-----END PUBLIC KEY-----`; + +describe('PublicKey', () => { + let app: App; + let stack: Stack; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '123456789012', region: 'testregion' }, + }); + }); + + test('import existing key group by id', () => { + const publicKeyId = 'K36X4X2EO997HM'; + const pubKey = PublicKey.fromPublicKeyId(stack, 'MyPublicKey', publicKeyId); + expect(pubKey.publicKeyId).toEqual(publicKeyId); + }); + + test('minimal example', () => { + new PublicKey(stack, 'MyPublicKey', { + encodedKey: publicKey, + }); + + expectStack(stack).toMatch({ + Resources: { + MyPublicKey78071F3D: { + Type: 'AWS::CloudFront::PublicKey', + Properties: { + PublicKeyConfig: { + CallerReference: 'c872d91ae0d2943aad25d4b31f1304d0a62c658ace', + EncodedKey: publicKey, + Name: 'StackMyPublicKey36EDA6AB', + }, + }, + }, + }, + }); + }); + + test('maximum example', () => { + new PublicKey(stack, 'MyPublicKey', { + publicKeyName: 'pub-key', + encodedKey: publicKey, + comment: 'Key expiring on 1/1/1984', + }); + + expectStack(stack).toMatch({ + Resources: { + MyPublicKey78071F3D: { + Type: 'AWS::CloudFront::PublicKey', + Properties: { + PublicKeyConfig: { + CallerReference: 'c872d91ae0d2943aad25d4b31f1304d0a62c658ace', + Comment: 'Key expiring on 1/1/1984', + EncodedKey: publicKey, + Name: 'pub-key', + }, + }, + }, + }, + }); + }); + + test('bad key example', () => { + expect(() => new PublicKey(stack, 'MyPublicKey', { + publicKeyName: 'pub-key', + encodedKey: 'bad-key', + comment: 'Key expiring on 1/1/1984', + })).toThrow(/Public key must be in PEM format [(]with the BEGIN\/END PUBLIC KEY lines[)]; got (.*?)/); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/web_distribution.test.ts b/packages/@aws-cdk/aws-cloudfront/test/web-distribution.test.ts similarity index 96% rename from packages/@aws-cdk/aws-cloudfront/test/web_distribution.test.ts rename to packages/@aws-cdk/aws-cloudfront/test/web-distribution.test.ts index ea2fbf3f22295..8eea2cad5bc92 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/web_distribution.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/web-distribution.test.ts @@ -8,7 +8,9 @@ import { CfnDistribution, CloudFrontWebDistribution, GeoRestriction, + KeyGroup, LambdaEdgeEventType, + PublicKey, SecurityPolicyProtocol, SSLMethod, ViewerCertificate, @@ -17,6 +19,16 @@ import { /* eslint-disable quote-props */ +const publicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAudf8/iNkQgdvjEdm6xYS +JAyxd/kGTbJfQNg9YhInb7TSm0dGu0yx8yZ3fnpmxuRPqJIlaVr+fT4YRl71gEYa +dlhHmnVegyPNjP9dNqZ7zwNqMEPOPnS/NOHbJj1KYKpn1f8pPNycQ5MQCntKGnSj +6fc+nbcC0joDvGz80xuy1W4hLV9oC9c3GT26xfZb2jy9MVtA3cppNuTwqrFi3t6e +0iGpraxZlT5wewjZLpQkngqYr6s3aucPAZVsGTEYPo4nD5mswmtZOm+tgcOrivtD +/3sD/qZLQ6c5siqyS8aTraD6y+VXugujfarTU65IeZ6QAUbLMsWuZOIi5Jn8zAwx +NQIDAQAB +-----END PUBLIC KEY-----`; + nodeunitShim({ 'distribution with custom origin adds custom origin'(test: Test) { @@ -188,6 +200,14 @@ nodeunitShim({ 'distribution with trusted signers on default distribution'(test: Test) { const stack = new cdk.Stack(); const sourceBucket = new s3.Bucket(stack, 'Bucket'); + const pubKey = new PublicKey(stack, 'MyPubKey', { + encodedKey: publicKey, + }); + const keyGroup = new KeyGroup(stack, 'MyKeyGroup', { + items: [ + pubKey, + ], + }); new CloudFrontWebDistribution(stack, 'AnAmazingWebsiteProbably', { originConfigs: [ @@ -199,6 +219,9 @@ nodeunitShim({ { isDefaultBehavior: true, trustedSigners: ['1234'], + trustedKeyGroups: [ + keyGroup, + ], }, ], }, @@ -212,6 +235,29 @@ nodeunitShim({ 'DeletionPolicy': 'Retain', 'UpdateReplacePolicy': 'Retain', }, + 'MyPubKey6ADA4CF5': { + 'Type': 'AWS::CloudFront::PublicKey', + 'Properties': { + 'PublicKeyConfig': { + 'CallerReference': 'c8141e732ea37b19375d4cbef2b2d2c6f613f0649a', + 'EncodedKey': publicKey, + 'Name': 'MyPubKey', + }, + }, + }, + 'MyKeyGroupAF22FD35': { + 'Type': 'AWS::CloudFront::KeyGroup', + 'Properties': { + 'KeyGroupConfig': { + 'Items': [ + { + 'Ref': 'MyPubKey6ADA4CF5', + }, + ], + 'Name': 'MyKeyGroup', + }, + }, + }, 'AnAmazingWebsiteProbablyCFDistribution47E3983B': { 'Type': 'AWS::CloudFront::Distribution', 'Properties': { @@ -250,9 +296,12 @@ nodeunitShim({ 'QueryString': false, 'Cookies': { 'Forward': 'none' }, }, - 'TrustedSigners': [ - '1234', + 'TrustedKeyGroups': [ + { + 'Ref': 'MyKeyGroupAF22FD35', + }, ], + 'TrustedSigners': ['1234'], 'Compress': true, }, 'Enabled': true, diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index 2dd9a2fbc7971..7dda406e626be 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -72,7 +72,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "aws-sdk": "^2.830.0", + "aws-sdk": "^2.842.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm-action.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm-action.ts index 7b48d0f055873..fb7afe98d7725 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm-action.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm-action.ts @@ -1,6 +1,10 @@ -import { Construct } from '@aws-cdk/core'; + import { IAlarm } from './alarm-base'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Interface for objects that can be the targets of CloudWatch alarm actions */ diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts b/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts index f232b5d27edf5..98f8980db4f4b 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/graph.ts @@ -114,7 +114,7 @@ export class AlarmWidget extends ConcreteWidget { alarms: [this.props.alarm.alarmArn], }, yAxis: { - left: this.props.leftYAxis !== undefined ? this.props.leftYAxis : undefined, + left: this.props.leftYAxis ?? undefined, }, }, }]; @@ -271,8 +271,8 @@ export class GraphWidget extends ConcreteWidget { metrics: metrics.length > 0 ? metrics : undefined, annotations: horizontalAnnotations.length > 0 ? { horizontal: horizontalAnnotations } : undefined, yAxis: { - left: this.props.leftYAxis !== undefined ? this.props.leftYAxis : undefined, - right: this.props.rightYAxis !== undefined ? this.props.rightYAxis : undefined, + left: this.props.leftYAxis ?? undefined, + right: this.props.rightYAxis ?? undefined, }, legend: this.props.legendPosition !== undefined ? { position: this.props.legendPosition } : undefined, liveData: this.props.liveData, diff --git a/packages/@aws-cdk/aws-codebuild/README.md b/packages/@aws-cdk/aws-codebuild/README.md index 7eff891a9ca59..9f35a81656b56 100644 --- a/packages/@aws-cdk/aws-codebuild/README.md +++ b/packages/@aws-cdk/aws-codebuild/README.md @@ -600,3 +600,20 @@ new codebuild.Project(stack, 'MyProject', { Here's a CodeBuild project with a simple example that creates a project mounted on AWS EFS: [Minimal Example](./test/integ.project-file-system-location.ts) + +## Batch builds + +To enable batch builds you should call `enableBatchBuilds()` on the project instance. + +It returns an object containing the batch service role that was created, +or `undefined` if batch builds could not be enabled, for example if the project was imported. + +```ts +import * as codebuild from '@aws-cdk/aws-codebuild'; + +const project = new codebuild.Project(this, 'MyProject', { ... }); + +if (project.enableBatchBuilds()) { + console.log('Batch builds were enabled'); +} +``` diff --git a/packages/@aws-cdk/aws-codebuild/lib/index.ts b/packages/@aws-cdk/aws-codebuild/lib/index.ts index 96731b2130043..5c2de5f3119c2 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/index.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/index.ts @@ -10,6 +10,7 @@ export * from './cache'; export * from './build-spec'; export * from './file-location'; export * from './linux-gpu-build-image'; +export * from './untrusted-code-boundary-policy'; // AWS::CodeBuild CloudFormation Resources: export * from './codebuild.generated'; diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 796422b379782..2f31bc897f9f8 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -28,6 +28,14 @@ import { CODEPIPELINE_SOURCE_ARTIFACTS_TYPE, NO_SOURCE_TYPE } from './source-typ // eslint-disable-next-line import { Construct as CoreConstruct } from '@aws-cdk/core'; +/** + * The type returned from {@link IProject#enableBatchBuilds}. + */ +export interface BatchBuildConfig { + /** The IAM batch service Role of this Project. */ + readonly role: iam.IRole; +} + export interface IProject extends IResource, iam.IGrantable, ec2.IConnectable { /** * The ARN of this Project. @@ -44,6 +52,14 @@ export interface IProject extends IResource, iam.IGrantable, ec2.IConnectable { /** The IAM service Role of this Project. Undefined for imported Projects. */ readonly role?: iam.IRole; + /** + * Enable batch builds. + * + * Returns an object contining the batch service role if batch builds + * could be enabled. + */ + enableBatchBuilds(): BatchBuildConfig | undefined; + addToRolePolicy(policyStatement: iam.PolicyStatement): void; /** @@ -196,6 +212,10 @@ abstract class ProjectBase extends Resource implements IProject { return this._connections; } + public enableBatchBuilds(): BatchBuildConfig | undefined { + return undefined; + } + /** * Add a permission only if there's a policy attached. * @param statement The permissions statement to add @@ -675,9 +695,11 @@ export class Project extends ProjectBase { * @returns an array of {@link CfnProject.EnvironmentVariableProperty} instances */ public static serializeEnvVariables(environmentVariables: { [name: string]: BuildEnvironmentVariable }, - validateNoPlainTextSecrets: boolean = false): CfnProject.EnvironmentVariableProperty[] { + validateNoPlainTextSecrets: boolean = false, principal?: iam.IGrantable): CfnProject.EnvironmentVariableProperty[] { const ret = new Array(); + const ssmVariables = new Array(); + const secretsManagerSecrets = new Array(); for (const [name, envVariable] of Object.entries(environmentVariables)) { const cfnEnvVariable: CfnProject.EnvironmentVariableProperty = { @@ -700,6 +722,46 @@ export class Project extends ProjectBase { } } } + + if (principal) { + // save the SSM env variables + if (envVariable.type === BuildEnvironmentVariableType.PARAMETER_STORE) { + const envVariableValue = envVariable.value.toString(); + ssmVariables.push(Stack.of(principal).formatArn({ + service: 'ssm', + resource: 'parameter', + // If the parameter name starts with / the resource name is not separated with a double '/' + // arn:aws:ssm:region:1111111111:parameter/PARAM_NAME + resourceName: envVariableValue.startsWith('/') + ? envVariableValue.substr(1) + : envVariableValue, + })); + } + + // save SecretsManager env variables + if (envVariable.type === BuildEnvironmentVariableType.SECRETS_MANAGER) { + secretsManagerSecrets.push(Stack.of(principal).formatArn({ + service: 'secretsmanager', + resource: 'secret', + // we don't know the exact ARN of the Secret just from its name, but we can get close + resourceName: `${envVariable.value}-??????`, + sep: ':', + })); + } + } + } + + if (ssmVariables.length !== 0) { + principal?.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['ssm:GetParameters'], + resources: ssmVariables, + })); + } + if (secretsManagerSecrets.length !== 0) { + principal?.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['secretsmanager:GetSecretValue'], + resources: secretsManagerSecrets, + })); } return ret; @@ -729,6 +791,7 @@ export class Project extends ProjectBase { private readonly _secondaryArtifacts: CfnProject.ArtifactsProperty[]; private _encryptionKey?: kms.IKey; private readonly _fileSystemLocations: CfnProject.ProjectFileSystemLocationProperty[]; + private _batchServiceRole?: iam.Role; constructor(scope: Construct, id: string, props: ProjectProps) { super(scope, id, { @@ -813,6 +876,14 @@ export class Project extends ProjectBase { sourceVersion: sourceConfig.sourceVersion, vpcConfig: this.configureVpc(props), logsConfig: this.renderLoggingConfiguration(props.logging), + buildBatchConfig: Lazy.any({ + produce: () => { + const config: CfnProject.ProjectBuildBatchConfigProperty | undefined = this._batchServiceRole ? { + serviceRole: this._batchServiceRole.roleArn, + } : undefined; + return config; + }, + }), }); this.addVpcRequiredPermissions(props, resource); @@ -825,7 +896,6 @@ export class Project extends ProjectBase { this.projectName = this.getResourceNameAttribute(resource.ref); this.addToRolePolicy(this.createLoggingPermission()); - this.addEnvVariablesPermissions(props.environmentVariables); // add permissions to create and use test report groups // with names starting with the project's name, // unless the customer explicitly opts out of it @@ -853,6 +923,27 @@ export class Project extends ProjectBase { } } + public enableBatchBuilds(): BatchBuildConfig | undefined { + if (!this._batchServiceRole) { + this._batchServiceRole = new iam.Role(this, 'BatchServiceRole', { + assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'), + }); + this._batchServiceRole.addToPrincipalPolicy(new iam.PolicyStatement({ + resources: [Lazy.string({ + produce: () => this.projectArn, + })], + actions: [ + 'codebuild:StartBuild', + 'codebuild:StopBuild', + 'codebuild:RetryBuild', + ], + })); + } + return { + role: this._batchServiceRole, + }; + } + /** * Adds a secondary source to the Project. * @@ -957,57 +1048,6 @@ export class Project extends ProjectBase { }); } - private addEnvVariablesPermissions(environmentVariables: { [name: string]: BuildEnvironmentVariable } | undefined): void { - this.addParameterStorePermissions(environmentVariables); - this.addSecretsManagerPermissions(environmentVariables); - } - - private addParameterStorePermissions(environmentVariables: { [name: string]: BuildEnvironmentVariable } | undefined): void { - const resources = Object.values(environmentVariables || {}) - .filter(envVariable => envVariable.type === BuildEnvironmentVariableType.PARAMETER_STORE) - .map(envVariable => - // If the parameter name starts with / the resource name is not separated with a double '/' - // arn:aws:ssm:region:1111111111:parameter/PARAM_NAME - (envVariable.value as string).startsWith('/') - ? (envVariable.value as string).substr(1) - : envVariable.value) - .map(envVariable => Stack.of(this).formatArn({ - service: 'ssm', - resource: 'parameter', - resourceName: envVariable, - })); - - if (resources.length === 0) { - return; - } - - this.addToRolePolicy(new iam.PolicyStatement({ - actions: ['ssm:GetParameters'], - resources, - })); - } - - private addSecretsManagerPermissions(environmentVariables: { [name: string]: BuildEnvironmentVariable } | undefined): void { - const resources = Object.values(environmentVariables || {}) - .filter(envVariable => envVariable.type === BuildEnvironmentVariableType.SECRETS_MANAGER) - .map(envVariable => Stack.of(this).formatArn({ - service: 'secretsmanager', - resource: 'secret', - // we don't know the exact ARN of the Secret just from its name, but we can get close - resourceName: `${envVariable.value}-??????`, - sep: ':', - })); - - if (resources.length === 0) { - return; - } - - this.addToRolePolicy(new iam.PolicyStatement({ - actions: ['secretsmanager:GetSecretValue'], - resources, - })); - } - private renderEnvironment( props: ProjectProps, projectVars: { [name: string]: BuildEnvironmentVariable } = {}): CfnProject.EnvironmentProperty { @@ -1068,7 +1108,7 @@ export class Project extends ProjectBase { privilegedMode: env.privileged || false, computeType: env.computeType || this.buildImage.defaultComputeType, environmentVariables: hasEnvironmentVars - ? Project.serializeEnvVariables(vars, props.checkSecretsInPlainTextEnvVariables ?? true) + ? Project.serializeEnvVariables(vars, props.checkSecretsInPlainTextEnvVariables ?? true, this) : undefined, }; } @@ -1139,8 +1179,8 @@ export class Project extends ProjectBase { return undefined; } - let s3Config: CfnProject.S3LogsConfigProperty|undefined = undefined; - let cloudwatchConfig: CfnProject.CloudWatchLogsConfigProperty|undefined = undefined; + let s3Config: CfnProject.S3LogsConfigProperty | undefined = undefined; + let cloudwatchConfig: CfnProject.CloudWatchLogsConfigProperty | undefined = undefined; if (props.s3) { const s3Logs = props.s3; @@ -1350,10 +1390,10 @@ export interface IBuildImage { } /** Optional arguments to {@link IBuildImage.binder} - currently empty. */ -export interface BuildImageBindOptions {} +export interface BuildImageBindOptions { } /** The return type from {@link IBuildImage.binder} - currently empty. */ -export interface BuildImageConfig {} +export interface BuildImageConfig { } // @deprecated(not in tsdoc on purpose): add bind() to IBuildImage // and get rid of IBindableBuildImage diff --git a/packages/@aws-cdk/aws-codebuild/lib/source.ts b/packages/@aws-cdk/aws-codebuild/lib/source.ts index b8002f2db9e36..19161ef9b6172 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/source.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/source.ts @@ -485,6 +485,8 @@ interface ThirdPartyGitSourceProps extends GitSourceProps { /** * Trigger a batch build from a webhook instead of a standard one. * + * Enabling this will enable batch builds on the CodeBuild project. + * * @default false */ readonly webhookTriggersBatchBuild?: boolean; @@ -513,14 +515,14 @@ abstract class ThirdPartyGitSource extends GitSource { super(props); this.webhook = props.webhook; - this.reportBuildStatus = props.reportBuildStatus === undefined ? true : props.reportBuildStatus; + this.reportBuildStatus = props.reportBuildStatus ?? true; this.webhookFilters = props.webhookFilters || []; this.webhookTriggersBatchBuild = props.webhookTriggersBatchBuild; } - public bind(_scope: CoreConstruct, _project: IProject): SourceConfig { + public bind(_scope: CoreConstruct, project: IProject): SourceConfig { const anyFilterGroupsProvided = this.webhookFilters.length > 0; - const webhook = this.webhook === undefined ? (anyFilterGroupsProvided ? true : undefined) : this.webhook; + const webhook = this.webhook ?? (anyFilterGroupsProvided ? true : undefined); if (!webhook && anyFilterGroupsProvided) { throw new Error('`webhookFilters` cannot be used when `webhook` is `false`'); @@ -530,7 +532,12 @@ abstract class ThirdPartyGitSource extends GitSource { throw new Error('`webhookTriggersBatchBuild` cannot be used when `webhook` is `false`'); } - const superConfig = super.bind(_scope, _project); + const superConfig = super.bind(_scope, project); + + if (this.webhookTriggersBatchBuild) { + project.enableBatchBuilds(); + } + return { sourceProperty: { ...superConfig.sourceProperty, diff --git a/packages/@aws-cdk/aws-codebuild/lib/untrusted-code-boundary-policy.ts b/packages/@aws-cdk/aws-codebuild/lib/untrusted-code-boundary-policy.ts new file mode 100644 index 0000000000000..229cb547e7c1f --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/lib/untrusted-code-boundary-policy.ts @@ -0,0 +1,94 @@ +import * as iam from '@aws-cdk/aws-iam'; +import { Construct } from 'constructs'; + +/** + * Construction properties for UntrustedCodeBoundaryPolicy + */ +export interface UntrustedCodeBoundaryPolicyProps { + /** + * The name of the managed policy. + * + * @default - A name is automatically generated. + */ + readonly managedPolicyName?: string; + + /** + * Additional statements to add to the default set of statements + * + * @default - No additional statements + */ + readonly additionalStatements?: iam.PolicyStatement[]; +} + +/** + * Permissions Boundary for a CodeBuild Project running untrusted code + * + * This class is a Policy, intended to be used as a Permissions Boundary + * for a CodeBuild project. It allows most of the actions necessary to run + * the CodeBuild project, but disallows reading from Parameter Store + * and Secrets Manager. + * + * Use this when your CodeBuild project is running untrusted code (for + * example, if you are using one to automatically build Pull Requests + * that anyone can submit), and you want to prevent your future self + * from accidentally exposing Secrets to this build. + * + * (The reason you might want to do this is because otherwise anyone + * who can submit a Pull Request to your project can write a script + * to email those secrets to themselves). + * + * @example + * + * iam.PermissionsBoundary.of(project).apply(new UntrustedCodeBoundaryPolicy(this, 'Boundary')); + */ +export class UntrustedCodeBoundaryPolicy extends iam.ManagedPolicy { + constructor(scope: Construct, id: string, props: UntrustedCodeBoundaryPolicyProps = {}) { + super(scope, id, { + managedPolicyName: props.managedPolicyName, + description: 'Permissions Boundary Policy for CodeBuild Projects running untrusted code', + statements: [ + new iam.PolicyStatement({ + actions: [ + // For logging + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + + // For test reports + 'codebuild:CreateReportGroup', + 'codebuild:CreateReport', + 'codebuild:UpdateReport', + 'codebuild:BatchPutTestCases', + 'codebuild:BatchPutCodeCoverages', + + // For batch builds + 'codebuild:StartBuild', + 'codebuild:StopBuild', + 'codebuild:RetryBuild', + + // For pulling ECR images + 'ecr:GetDownloadUrlForLayer', + 'ecr:BatchGetImage', + 'ecr:BatchCheckLayerAvailability', + + // For running in a VPC + 'ec2:CreateNetworkInterfacePermission', + 'ec2:CreateNetworkInterface', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DeleteNetworkInterface', + 'ec2:DescribeSubnets', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeDhcpOptions', + 'ec2:DescribeVpcs', + + // NOTABLY MISSING: + // - Reading secrets + // - Reading parameterstore + ], + resources: ['*'], + }), + ...props.additionalStatements ?? [], + ], + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index 7c378d037ce4c..314210113d2b7 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -78,7 +78,7 @@ "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.830.0", + "aws-sdk": "^2.842.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -86,7 +86,6 @@ "pkglint": "0.0.0" }, "dependencies": { - "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", @@ -105,7 +104,6 @@ }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { - "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.github-webhook-batch.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.github-webhook-batch.expected.json new file mode 100644 index 0000000000000..b58305bddf2f3 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.github-webhook-batch.expected.json @@ -0,0 +1,188 @@ +{ + "Resources": { + "MyProjectRole9BBE5233": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyProjectRoleDefaultPolicyB19B7C29": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "MyProject39F7B0AE" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "MyProject39F7B0AE" + }, + ":*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyProjectRoleDefaultPolicyB19B7C29", + "Roles": [ + { + "Ref": "MyProjectRole9BBE5233" + } + ] + } + }, + "MyProjectBatchServiceRole6B35CF0E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyProjectBatchServiceRoleDefaultPolicy7A0E5721": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:StartBuild", + "codebuild:StopBuild", + "codebuild:RetryBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyProject39F7B0AE", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyProjectBatchServiceRoleDefaultPolicy7A0E5721", + "Roles": [ + { + "Ref": "MyProjectBatchServiceRole6B35CF0E" + } + ] + } + }, + "MyProject39F7B0AE": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "NO_ARTIFACTS" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", + "PrivilegedMode": false, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "MyProjectRole9BBE5233", + "Arn" + ] + }, + "Source": { + "Location": "https://github.com/aws/aws-cdk.git", + "ReportBuildStatus": false, + "Type": "GITHUB" + }, + "BuildBatchConfig": { + "ServiceRole": { + "Fn::GetAtt": [ + "MyProjectBatchServiceRole6B35CF0E", + "Arn" + ] + } + }, + "EncryptionKey": "alias/aws/s3", + "Triggers": { + "BuildType": "BUILD_BATCH", + "FilterGroups": [ + [ + { + "Pattern": "PUSH", + "Type": "EVENT" + } + ] + ], + "Webhook": true + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.github-webhook-batch.ts b/packages/@aws-cdk/aws-codebuild/test/integ.github-webhook-batch.ts new file mode 100644 index 0000000000000..d8fd006956035 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.github-webhook-batch.ts @@ -0,0 +1,29 @@ +import * as cdk from '@aws-cdk/core'; +import * as codebuild from '../lib'; + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string) { + super(scope, id); + + const source = codebuild.Source.gitHub({ + owner: 'aws', + repo: 'aws-cdk', + reportBuildStatus: false, + webhook: true, + webhookTriggersBatchBuild: true, + webhookFilters: [ + codebuild.FilterGroup.inEventOf(codebuild.EventAction.PUSH), + ], + }); + new codebuild.Project(this, 'MyProject', { + source, + grantReportGroupPermissions: false, + }); + } +} + +const app = new cdk.App(); + +new TestStack(app, 'test-codebuild-github-webhook-batch'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts index 5a7766918c67b..d6341771d71b7 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts @@ -712,6 +712,51 @@ export = { Webhook: true, BuildType: 'BUILD_BATCH', }, + BuildBatchConfig: { + ServiceRole: { + 'Fn::GetAtt': [ + 'ProjectBatchServiceRoleF97A1CFB', + 'Arn', + ], + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codebuild.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + })); + + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'codebuild:StartBuild', + 'codebuild:StopBuild', + 'codebuild:RetryBuild', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'ProjectC78D97AD', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, })); test.done(); @@ -1275,12 +1320,11 @@ export = { }); expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { - 'Artifacts': - { - 'Name': ABSENT, - 'ArtifactIdentifier': 'artifact1', - 'OverrideArtifactName': true, - }, + 'Artifacts': { + 'Name': ABSENT, + 'ArtifactIdentifier': 'artifact1', + 'OverrideArtifactName': true, + }, })); test.done(); @@ -1302,12 +1346,11 @@ export = { }); expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { - 'Artifacts': - { - 'ArtifactIdentifier': 'artifact1', - 'Name': 'specificname', - 'OverrideArtifactName': ABSENT, - }, + 'Artifacts': { + 'ArtifactIdentifier': 'artifact1', + 'Name': 'specificname', + 'OverrideArtifactName': ABSENT, + }, })); test.done(); @@ -1481,7 +1524,7 @@ export = { '', [ '111', - { twotwotwo: '222' }, + { twotwotwo: '222' }, ], ], }, @@ -1862,4 +1905,70 @@ export = { }, }, }, + + 'enableBatchBuilds()'(test: Test) { + const stack = new cdk.Stack(); + + const project = new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ + owner: 'testowner', + repo: 'testrepo', + }), + }); + + const returnVal = project.enableBatchBuilds(); + if (!returnVal?.role) { + throw new Error('Expecting return value with role'); + } + + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + BuildBatchConfig: { + ServiceRole: { + 'Fn::GetAtt': [ + 'ProjectBatchServiceRoleF97A1CFB', + 'Arn', + ], + }, + }, + })); + + expect(stack).to(haveResourceLike('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codebuild.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + })); + + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'codebuild:StartBuild', + 'codebuild:StopBuild', + 'codebuild:RetryBuild', + ], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'ProjectC78D97AD', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-codebuild/test/test.untrusted-code-boundary.ts b/packages/@aws-cdk/aws-codebuild/test/test.untrusted-code-boundary.ts new file mode 100644 index 0000000000000..04196e631f5c8 --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/test.untrusted-code-boundary.ts @@ -0,0 +1,56 @@ +import { expect, haveResourceLike, arrayWith } from '@aws-cdk/assert'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as codebuild from '../lib'; + +export = { + 'can attach permissions boundary to Project'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const project = new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ owner: 'a', repo: 'b' }), + }); + iam.PermissionsBoundary.of(project).apply(new codebuild.UntrustedCodeBoundaryPolicy(stack, 'Boundary')); + + // THEN + expect(stack).to(haveResourceLike('AWS::IAM::Role', { + PermissionsBoundary: { Ref: 'BoundaryEA298153' }, + })); + + test.done(); + }, + + 'can add additional statements Boundary'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const project = new codebuild.Project(stack, 'Project', { + source: codebuild.Source.gitHub({ owner: 'a', repo: 'b' }), + }); + iam.PermissionsBoundary.of(project).apply(new codebuild.UntrustedCodeBoundaryPolicy(stack, 'Boundary', { + additionalStatements: [ + new iam.PolicyStatement({ + actions: ['a:a'], + resources: ['b'], + }), + ], + })); + + // THEN + expect(stack).to(haveResourceLike('AWS::IAM::ManagedPolicy', { + PolicyDocument: { + Statement: arrayWith({ + Effect: 'Allow', + Action: 'a:a', + Resource: 'b', + }), + }, + })); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 6f617c91a0183..051cb15db124e 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -78,7 +78,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.830.0", + "aws-sdk": "^2.842.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 20822a0d2356b..55077fe93f273 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 @@ -99,9 +99,8 @@ export class CustomLambdaDeploymentConfig extends Resource implements ILambdaDep // Generates the name of the deployment config. It's also what you'll see in the AWS console // The name of the config is .LambdaPercentMinutes // Unless the user provides an explicit name - this.deploymentConfigName = props.deploymentConfigName !== undefined - ? props.deploymentConfigName - : `${Names.uniqueId(this)}.Lambda${props.type}${props.percentage}Percent${props.type === CustomLambdaDeploymentConfigType.LINEAR + this.deploymentConfigName = props.deploymentConfigName + ?? `${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-codedeploy/lib/server/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts index eb4bbafade216..32a3ccb3e40d1 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/server/deployment-group.ts @@ -277,7 +277,7 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { }); this._autoScalingGroups = props.autoScalingGroups || []; - this.installAgent = props.installAgent === undefined ? true : props.installAgent; + this.installAgent = props.installAgent ?? true; this.codeDeployBucket = s3.Bucket.fromBucketName(this, 'Bucket', `aws-codedeploy-${cdk.Stack.of(this).region}`); for (const asg of this._autoScalingGroups) { this.addCodeDeployAgentInstallUserData(asg); @@ -348,15 +348,20 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { switch (asg.osType) { case ec2.OperatingSystemType.LINUX: asg.addUserData( + 'set +e', // make sure we don't exit on the `which` failing 'PKG_CMD=`which yum 2>/dev/null`', + 'set -e', // continue with failing on error 'if [ -z "$PKG_CMD" ]; then', 'PKG_CMD=apt-get', 'else', 'PKG=CMD=yum', 'fi', '$PKG_CMD update -y', + 'set +e', // make sure we don't exit on the next command failing (we check its exit code below) '$PKG_CMD install -y ruby2.0', - 'if [ $? -ne 0 ]; then', + 'RUBY2_INSTALL=$?', + 'set -e', // continue with failing on error + 'if [ $RUBY2_INSTALL -ne 0 ]; then', '$PKG_CMD install -y ruby', 'fi', '$PKG_CMD install -y awscli', @@ -372,7 +377,8 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupBase { asg.addUserData( 'Set-Variable -Name TEMPDIR -Value (New-TemporaryFile).DirectoryName', `aws s3 cp s3://aws-codedeploy-${cdk.Stack.of(this).region}/latest/codedeploy-agent.msi $TEMPDIR\\codedeploy-agent.msi`, - '$TEMPDIR\\codedeploy-agent.msi /quiet /l c:\\temp\\host-agent-install-log.txt', + 'cd $TEMPDIR', + '.\\codedeploy-agent.msi /quiet /l c:\\temp\\host-agent-install-log.txt', ); break; } diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/integ.deployment-group.expected.json b/packages/@aws-cdk/aws-codedeploy/test/server/integ.deployment-group.expected.json index c463bc84e8df3..e50bc6ef02f2b 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/server/integ.deployment-group.expected.json +++ b/packages/@aws-cdk/aws-codedeploy/test/server/integ.deployment-group.expected.json @@ -659,7 +659,7 @@ "Fn::Join": [ "", [ - "#!/bin/bash\nPKG_CMD=`which yum 2>/dev/null`\nif [ -z \"$PKG_CMD\" ]; then\nPKG_CMD=apt-get\nelse\nPKG=CMD=yum\nfi\n$PKG_CMD update -y\n$PKG_CMD install -y ruby2.0\nif [ $? -ne 0 ]; then\n$PKG_CMD install -y ruby\nfi\n$PKG_CMD install -y awscli\nTMP_DIR=`mktemp -d`\ncd $TMP_DIR\naws s3 cp s3://aws-codedeploy-", + "#!/bin/bash\nset +e\nPKG_CMD=`which yum 2>/dev/null`\nset -e\nif [ -z \"$PKG_CMD\" ]; then\nPKG_CMD=apt-get\nelse\nPKG=CMD=yum\nfi\n$PKG_CMD update -y\nset +e\n$PKG_CMD install -y ruby2.0\nRUBY2_INSTALL=$?\nset -e\nif [ $RUBY2_INSTALL -ne 0 ]; then\n$PKG_CMD install -y ruby\nfi\n$PKG_CMD install -y awscli\nTMP_DIR=`mktemp -d`\ncd $TMP_DIR\naws s3 cp s3://aws-codedeploy-", { "Ref": "AWS::Region" }, @@ -884,4 +884,4 @@ "Default": "/aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2" } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts index fd486f20f1eb2..aebb3aa8a074f 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/server/test.deployment-group.ts @@ -42,6 +42,78 @@ export = { test.done(); }, + 'uses good linux install agent script'(test: Test) { + const stack = new cdk.Stack(); + + const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.STANDARD3, ec2.InstanceSize.SMALL), + machineImage: new ec2.AmazonLinuxImage(), + vpc: new ec2.Vpc(stack, 'VPC'), + }); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + autoScalingGroups: [asg], + installAgent: true, + }); + + expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + 'UserData': { + 'Fn::Base64': { + 'Fn::Join': [ + '', + [ + '#!/bin/bash\nset +e\nPKG_CMD=`which yum 2>/dev/null`\nset -e\nif [ -z "$PKG_CMD" ]; then\nPKG_CMD=apt-get\nelse\nPKG=CMD=yum\nfi\n$PKG_CMD update -y\nset +e\n$PKG_CMD install -y ruby2.0\nRUBY2_INSTALL=$?\nset -e\nif [ $RUBY2_INSTALL -ne 0 ]; then\n$PKG_CMD install -y ruby\nfi\n$PKG_CMD install -y awscli\nTMP_DIR=`mktemp -d`\ncd $TMP_DIR\naws s3 cp s3://aws-codedeploy-', + { + 'Ref': 'AWS::Region', + }, + '/latest/install . --region ', + { + 'Ref': 'AWS::Region', + }, + '\nchmod +x ./install\n./install auto\nrm -fr $TMP_DIR', + ], + ], + }, + }, + })); + + test.done(); + }, + + 'uses good windows install agent script'(test: Test) { + const stack = new cdk.Stack(); + + const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.STANDARD3, ec2.InstanceSize.SMALL), + machineImage: new ec2.WindowsImage(ec2.WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_BASE, {}), + vpc: new ec2.Vpc(stack, 'VPC'), + }); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + autoScalingGroups: [asg], + installAgent: true, + }); + + expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + 'UserData': { + 'Fn::Base64': { + 'Fn::Join': [ + '', + [ + 'Set-Variable -Name TEMPDIR -Value (New-TemporaryFile).DirectoryName\naws s3 cp s3://aws-codedeploy-', + { + 'Ref': 'AWS::Region', + }, + '/latest/codedeploy-agent.msi $TEMPDIR\\codedeploy-agent.msi\ncd $TEMPDIR\n.\\codedeploy-agent.msi /quiet /l c:\\temp\\host-agent-install-log.txt', + ], + ], + }, + }, + })); + + test.done(); + }, + 'created with ASGs contains the ASG names'(test: Test) { const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/action.ts index b3ce90e2aa793..c2f7ca5ca5305 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/action.ts @@ -1,6 +1,10 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as events from '@aws-cdk/aws-events'; -import { Construct, Lazy } from '@aws-cdk/core'; +import { Lazy } from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; /** * Low-level class for generic CodePipeline Actions. diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/alexa-ask/deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/alexa-ask/deploy-action.ts index ebbe9fd060168..f5eb7c4e64579 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/alexa-ask/deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/alexa-ask/deploy-action.ts @@ -1,7 +1,11 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; -import { Construct, SecretValue } from '@aws-cdk/core'; +import { SecretValue } from '@aws-cdk/core'; import { Action } from '../action'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Construction properties of the {@link AlexaSkillDeployAction Alexa deploy Action}. */ diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts index 75f34777471e5..7d7625ab4fca4 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts @@ -1,9 +1,13 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; -import { Construct } from '@aws-cdk/core'; + import { Action } from '../action'; import { sourceArtifactBounds } from '../common'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Construction properties for {@link BitBucketSourceAction}. * 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 b4a300d887ad4..63618e086ed91 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 @@ -168,9 +168,25 @@ interface CloudFormationDeployActionProps extends CloudFormationActionProps { * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities * @default None, unless `adminPermissions` is true + * @deprecated use {@link cfnCapabilities} instead */ readonly capabilities?: cloudformation.CloudFormationCapabilities[]; + /** + * Acknowledge certain changes made as part of deployment. + * + * For stacks that contain certain resources, + * explicit acknowledgement is required that AWS CloudFormation might create or update those resources. + * For example, you must specify `ANONYMOUS_IAM` or `NAMED_IAM` if your stack template contains AWS + * Identity and Access Management (IAM) resources. + * For more information, see the link below. + * + * @default None, unless `adminPermissions` is true + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-template.html#using-iam-capabilities + */ + readonly cfnCapabilities?: cdk.CfnCapabilities[]; + /** * Whether to grant full permissions to CloudFormation while deploying this template. * @@ -301,9 +317,18 @@ abstract class CloudFormationDeployAction extends CloudFormationAction { SingletonPolicy.forRole(options.role).grantPassRole(this._deploymentRole); - const capabilities = this.props2.adminPermissions && this.props2.capabilities === undefined - ? [cloudformation.CloudFormationCapabilities.NAMED_IAM] - : this.props2.capabilities; + const providedCapabilities = this.props2.cfnCapabilities ?? + this.props2.capabilities?.map(c => { + switch (c) { + case cloudformation.CloudFormationCapabilities.NONE: return cdk.CfnCapabilities.NONE; + case cloudformation.CloudFormationCapabilities.ANONYMOUS_IAM: return cdk.CfnCapabilities.ANONYMOUS_IAM; + case cloudformation.CloudFormationCapabilities.NAMED_IAM: return cdk.CfnCapabilities.NAMED_IAM; + case cloudformation.CloudFormationCapabilities.AUTO_EXPAND: return cdk.CfnCapabilities.AUTO_EXPAND; + } + }); + const capabilities = this.props2.adminPermissions && providedCapabilities === undefined + ? [cdk.CfnCapabilities.NAMED_IAM] + : providedCapabilities; const actionConfig = super.bound(scope, stage, options); return { @@ -620,7 +645,7 @@ interface StatementTemplate { type StatementCondition = { [op: string]: { [attribute: string]: string } }; -function parseCapabilities(capabilities: cloudformation.CloudFormationCapabilities[] | undefined): string | undefined { +function parseCapabilities(capabilities: cdk.CfnCapabilities[] | undefined): string | undefined { if (capabilities === undefined) { return undefined; } else if (capabilities.length === 1) { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts index 468684664cc5f..54735d4ec1d9d 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts @@ -97,6 +97,8 @@ export interface CodeBuildActionProps extends codepipeline.CommonAwsActionProps /** * Trigger a batch build. * + * Enabling this will enable batch builds on the CodeBuild project. + * * @default false */ readonly executeBatchBuild?: boolean; @@ -154,7 +156,7 @@ export class CodeBuildAction extends Action { options.role.addToPolicy(new iam.PolicyStatement({ resources: [this.props.project.projectArn], actions: [ - 'codebuild:BatchGetBuilds', + `codebuild:${this.props.executeBatchBuild ? 'BatchGetBuildBatches' : 'BatchGetBuilds'}`, `codebuild:${this.props.executeBatchBuild ? 'StartBuildBatch' : 'StartBuild'}`, `codebuild:${this.props.executeBatchBuild ? 'StopBuildBatch' : 'StopBuild'}`, ], @@ -205,7 +207,7 @@ export class CodeBuildAction extends Action { ProjectName: this.props.project.projectName, EnvironmentVariables: this.props.environmentVariables && cdk.Stack.of(scope).toJsonString(codebuild.Project.serializeEnvVariables(this.props.environmentVariables, - this.props.checkSecretsInPlainTextEnvVariables ?? true)), + this.props.checkSecretsInPlainTextEnvVariables ?? true, this.props.project)), }; if ((this.actionProperties.inputs || []).length > 1) { // lazy, because the Artifact name might be generated lazily @@ -213,6 +215,7 @@ export class CodeBuildAction extends Action { } if (this.props.executeBatchBuild) { configuration.BatchEnabled = 'true'; + this.props.project.enableBatchBuilds(); } return { configuration, 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 2b18bb9db6071..602a168487830 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,10 +2,14 @@ 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, Names, Token } from '@aws-cdk/core'; +import { Names, Token } from '@aws-cdk/core'; import { Action } from '../action'; import { sourceArtifactBounds } from '../common'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * How should the CodeCommit Action detect changes. * This is the type of the {@link CodeCommitSourceAction.trigger} property. diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/ecs-deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/ecs-deploy-action.ts index 33bc07c148b1e..dd9ff5247e828 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/ecs-deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/ecs-deploy-action.ts @@ -1,9 +1,13 @@ import * as codedeploy from '@aws-cdk/aws-codedeploy'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; -import { Construct, Lazy } from '@aws-cdk/core'; +import { Lazy } from '@aws-cdk/core'; import { Action } from '../action'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Configuration for replacing a placeholder string in the ECS task * definition template file with an image URI. diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/server-deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/server-deploy-action.ts index 07c0662824481..519a47708b74a 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/server-deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codedeploy/server-deploy-action.ts @@ -1,10 +1,14 @@ import * as codedeploy from '@aws-cdk/aws-codedeploy'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; -import { Construct } from '@aws-cdk/core'; + import { Action } from '../action'; import { deployArtifactBounds } from '../common'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Construction properties of the {@link CodeDeployServerDeployAction CodeDeploy server deploy CodePipeline Action}. */ diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/custom-action-registration.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/custom-action-registration.ts index 41e34ac8a0b5d..dad9e84a47094 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/custom-action-registration.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/custom-action-registration.ts @@ -132,7 +132,7 @@ export class CustomActionRegistration extends Construct { entityUrlTemplate: props.entityUrl, executionUrlTemplate: props.executionUrl, }, - configurationProperties: props.actionProperties === undefined ? undefined : props.actionProperties.map((ap) => { + configurationProperties: props.actionProperties?.map((ap) => { return { key: ap.key || false, secret: ap.secret || false, 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 f788c60d6aeea..6042d701017a5 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,10 +2,14 @@ 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, Names } from '@aws-cdk/core'; +import { Names } from '@aws-cdk/core'; import { Action } from '../action'; import { sourceArtifactBounds } from '../common'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * The CodePipeline variables emitted by the ECR source Action. */ diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts index ec5dfb7ad2999..22c9988ac3ff5 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecs/deploy-action.ts @@ -1,10 +1,14 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ecs from '@aws-cdk/aws-ecs'; import * as iam from '@aws-cdk/aws-iam'; -import { Construct, Duration } from '@aws-cdk/core'; +import { Duration } from '@aws-cdk/core'; import { Action } from '../action'; import { deployArtifactBounds } from '../common'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Construction properties of {@link EcsDeployAction}. */ diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts index 82ca0b033120c..96b90a0ecb19b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts @@ -1,8 +1,12 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; -import { Construct, SecretValue } from '@aws-cdk/core'; +import { SecretValue } from '@aws-cdk/core'; import { Action } from '../action'; import { sourceArtifactBounds } from '../common'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * If and how the GitHub source action should be triggered */ diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/jenkins/jenkins-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/jenkins/jenkins-action.ts index 5ebb614ea4ee7..81da65206b12a 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/jenkins/jenkins-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/jenkins/jenkins-action.ts @@ -1,8 +1,12 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; -import { Construct } from '@aws-cdk/core'; + import { Action } from '../action'; import { IJenkinsProvider, jenkinsArtifactsBounds } from './jenkins-provider'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * The type of the Jenkins Action that determines its CodePipeline Category - * Build, or Test. diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts index 987968728cb4d..f8a49d977f6b4 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/lambda/invoke-action.ts @@ -1,9 +1,13 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Construct, Stack } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { Action } from '../action'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Construction properties of the {@link LambdaInvokeAction Lambda invoke CodePipeline Action}. */ diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts index aebc8ba2548c8..75998456f4a39 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts @@ -1,10 +1,14 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; -import { Construct, Duration } from '@aws-cdk/core'; +import { Duration } from '@aws-cdk/core'; import { kebab as toKebabCase } from 'case'; import { Action } from '../action'; import { deployArtifactBounds } from '../common'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + // Class copied verbatim from the aws-s3-deployment module. // Yes, it sucks that we didn't abstract this properly in a common class, // but having 2 different CacheControl classes that behave differently would be worse I think. 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 2470ea6cca25f..cb86bd2591c6a 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,10 +1,14 @@ 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, Names, Token } from '@aws-cdk/core'; +import { Names, Token } from '@aws-cdk/core'; import { Action } from '../action'; import { sourceArtifactBounds } from '../common'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * How should the S3 Action detect changes. * This is the type of the {@link S3SourceAction.trigger} property. diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/servicecatalog/deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/servicecatalog/deploy-action.ts index f55766315c070..9059af1ac0ba1 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/servicecatalog/deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/servicecatalog/deploy-action.ts @@ -1,8 +1,12 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; -import { Construct } from '@aws-cdk/core'; + import { Action } from '../action'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Construction properties of the {@link ServiceCatalogDeployAction ServiceCatalog deploy CodePipeline Action}. * diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts index 31510ac049449..22bf46a15a641 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts @@ -558,6 +558,48 @@ export = { test.done(); }, + 'can use CfnCapabilities from the core module'(test: Test) { + // GIVEN + const stack = new TestFixture(); + + // WHEN + stack.deployStage.addAction(new cpactions.CloudFormationCreateUpdateStackAction({ + actionName: 'CreateUpdate', + stackName: 'MyStack', + templatePath: stack.sourceOutput.atPath('template.yaml'), + adminPermissions: false, + cfnCapabilities: [ + cdk.CfnCapabilities.NAMED_IAM, + cdk.CfnCapabilities.AUTO_EXPAND, + ], + })); + + // THEN: Action in Pipeline has named IAM and AUTOEXPAND capabilities + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + 'Stages': [ + { 'Name': 'Source' /* don't care about the rest */ }, + { + 'Name': 'Deploy', + 'Actions': [ + { + 'Configuration': { + 'Capabilities': 'CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND', + 'RoleArn': { 'Fn::GetAtt': ['PipelineDeployCreateUpdateRole515CB7D4', 'Arn'] }, + 'ActionMode': 'CREATE_UPDATE', + 'StackName': 'MyStack', + 'TemplatePath': 'SourceArtifact::template.yaml', + }, + 'InputArtifacts': [{ 'Name': 'SourceArtifact' }], + 'Name': 'CreateUpdate', + }, + ], + }, + ], + })); + + test.done(); + }, + 'cross-account CFN Pipeline': { 'correctly creates the deployment Role in the other account'(test: Test) { const app = new cdk.App(); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json index d489a5712eeba..3e9b5d76ec027 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-batch.expected.json @@ -164,7 +164,7 @@ }, { "Action": [ - "codebuild:BatchGetBuilds", + "codebuild:BatchGetBuildBatches", "codebuild:StartBuildBatch", "codebuild:StopBuildBatch" ], @@ -480,8 +480,63 @@ "Source": { "Type": "CODEPIPELINE" }, + "BuildBatchConfig": { + "ServiceRole": { + "Fn::GetAtt": [ + "MyBuildProjectBatchServiceRole531F3056", + "Arn" + ] + } + }, "EncryptionKey": "alias/aws/s3" } + }, + "MyBuildProjectBatchServiceRole531F3056": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyBuildProjectBatchServiceRoleDefaultPolicy816785FC": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codebuild:StartBuild", + "codebuild:StopBuild", + "codebuild:RetryBuild" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyBuildProject30DB9D6E", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyBuildProjectBatchServiceRoleDefaultPolicy816785FC", + "Roles": [ + { + "Ref": "MyBuildProjectBatchServiceRole531F3056" + } + ] + } } } } 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 7fe649e0c2f8c..dbf8c85f91394 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 @@ -149,6 +149,30 @@ ] } }, + { + "Action": "ssm:GetParameters", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ssm:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":parameter/param_store" + ] + ] + } + }, { "Action": [ "s3:GetObject*", @@ -898,4 +922,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-codepipeline/lib/action.ts b/packages/@aws-cdk/aws-codepipeline/lib/action.ts index b2b6e79154699..6b28bcc8a3749 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/action.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/action.ts @@ -1,9 +1,13 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; -import { Construct, IResource } from '@aws-cdk/core'; +import { IResource } from '@aws-cdk/core'; import { Artifact } from './artifact'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + export enum ActionCategory { SOURCE = 'Source', BUILD = 'Build', diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index ba7fd8d87233d..760e049c6ed73 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -3,7 +3,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 { - App, BootstraplessSynthesizer, Construct as CoreConstruct, DefaultStackSynthesizer, + App, BootstraplessSynthesizer, DefaultStackSynthesizer, IStackSynthesizer, Lazy, Names, PhysicalName, RemovalPolicy, Resource, Stack, Token, } from '@aws-cdk/core'; import { Construct } from 'constructs'; @@ -15,6 +15,10 @@ import { RichAction } from './private/rich-action'; import { Stage } from './private/stage'; import { validateName, validateNamespaceName, validateSourceAction } from './private/validation'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + /** * Allows you to control where to place a new Stage when it's added to the Pipeline. * Note that you can provide only one of the below properties - diff --git a/packages/@aws-cdk/aws-codepipeline/lib/private/full-action-descriptor.ts b/packages/@aws-cdk/aws-codepipeline/lib/private/full-action-descriptor.ts index 6d58bf815d3db..895d3cf15430c 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/private/full-action-descriptor.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/private/full-action-descriptor.ts @@ -36,13 +36,13 @@ export class FullActionDescriptor { this.owner = actionProperties.owner || 'AWS'; this.provider = actionProperties.provider; this.version = actionProperties.version || '1'; - this.runOrder = actionProperties.runOrder === undefined ? 1 : actionProperties.runOrder; + this.runOrder = actionProperties.runOrder ?? 1; this.artifactBounds = actionProperties.artifactBounds; this.namespace = actionProperties.variablesNamespace; this.inputs = deduplicateArtifacts(actionProperties.inputs); this.outputs = deduplicateArtifacts(actionProperties.outputs); this.region = props.actionRegion || actionProperties.region; - this.role = actionProperties.role !== undefined ? actionProperties.role : props.actionRole; + this.role = actionProperties.role ?? props.actionRole; this.configuration = props.actionConfig.configuration; } diff --git a/packages/@aws-cdk/aws-codepipeline/lib/private/rich-action.ts b/packages/@aws-cdk/aws-codepipeline/lib/private/rich-action.ts index f218344da61fa..e2cde045f88a3 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/private/rich-action.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/private/rich-action.ts @@ -1,7 +1,11 @@ import * as events from '@aws-cdk/aws-events'; -import { Construct, ResourceEnvironment, Stack, Token, TokenComparison } from '@aws-cdk/core'; +import { ResourceEnvironment, Stack, Token, TokenComparison } from '@aws-cdk/core'; import { ActionBindOptions, ActionConfig, ActionProperties, IAction, IPipeline, IStage } from '../action'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Helper routines to work with Actions * diff --git a/packages/@aws-cdk/aws-codepipeline/test/cross-env.test.ts b/packages/@aws-cdk/aws-codepipeline/test/cross-env.test.ts index 917f4c833858f..08fd0160b90f4 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/cross-env.test.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/cross-env.test.ts @@ -6,130 +6,136 @@ import * as codepipeline from '../lib'; import { FakeBuildAction } from './fake-build-action'; import { FakeSourceAction } from './fake-source-action'; -let app: App; -let stack: Stack; -let sourceArtifact: codepipeline.Artifact; -let initialStages: codepipeline.StageProps[]; - -beforeEach(() => { - app = new App(); - stack = new Stack(app, 'PipelineStack', { env: { account: '2222', region: 'us-east-1' } }); - sourceArtifact = new codepipeline.Artifact(); - initialStages = [ - { - stageName: 'Source', - actions: [new FakeSourceAction({ - actionName: 'Source', - output: sourceArtifact, - })], - }, - { - stageName: 'Build', - actions: [new FakeBuildAction({ - actionName: 'Build', - input: sourceArtifact, - })], - }, - ]; -}); - -describe('crossAccountKeys=false', () => { - let pipeline: codepipeline.Pipeline; +describe.each(['legacy', 'modern'])('with %s synthesis', (synthesisStyle: string) => { + let app: App; + let stack: Stack; + let sourceArtifact: codepipeline.Artifact; + let initialStages: codepipeline.StageProps[]; + beforeEach(() => { - pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { - crossAccountKeys: false, - stages: initialStages, + app = new App({ + context: { + ...synthesisStyle === 'modern' ? { '@aws-cdk/core:newStyleStackSynthesis': true } : undefined, + }, }); + stack = new Stack(app, 'PipelineStack', { env: { account: '2222', region: 'us-east-1' } }); + sourceArtifact = new codepipeline.Artifact(); + initialStages = [ + { + stageName: 'Source', + actions: [new FakeSourceAction({ + actionName: 'Source', + output: sourceArtifact, + })], + }, + { + stageName: 'Build', + actions: [new FakeBuildAction({ + actionName: 'Build', + input: sourceArtifact, + })], + }, + ]; }); - test('creates a bucket but no keys', () => { - // THEN - expect(stack).not.toHaveResource('AWS::KMS::Key'); - expect(stack).toHaveResource('AWS::S3::Bucket'); - }); - - describe('prevents adding a cross-account action', () => { - const expectedError = 'crossAccountKeys: true'; - - let stage: codepipeline.IStage; + describe('crossAccountKeys=false', () => { + let pipeline: codepipeline.Pipeline; beforeEach(() => { - stage = pipeline.addStage({ stageName: 'Deploy' }); + pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { + crossAccountKeys: false, + stages: initialStages, + }); }); - test('by role', () => { - // WHEN - expect(() => { - stage.addAction(new FakeBuildAction({ - actionName: 'Deploy', - input: sourceArtifact, - role: iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::1111:role/some-role'), - })); - }).toThrow(expectedError); + test('creates a bucket but no keys', () => { + // THEN + expect(stack).not.toHaveResource('AWS::KMS::Key'); + expect(stack).toHaveResource('AWS::S3::Bucket'); }); - test('by resource', () => { - // WHEN - expect(() => { - stage.addAction(new FakeBuildAction({ - actionName: 'Deploy', - input: sourceArtifact, - resource: s3.Bucket.fromBucketAttributes(stack, 'Bucket', { - bucketName: 'foo', + describe('prevents adding a cross-account action', () => { + const expectedError = 'crossAccountKeys: true'; + + let stage: codepipeline.IStage; + beforeEach(() => { + stage = pipeline.addStage({ stageName: 'Deploy' }); + }); + + test('by role', () => { + // WHEN + expect(() => { + stage.addAction(new FakeBuildAction({ + actionName: 'Deploy', + input: sourceArtifact, + role: iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::1111:role/some-role'), + })); + }).toThrow(expectedError); + }); + + test('by resource', () => { + // WHEN + expect(() => { + stage.addAction(new FakeBuildAction({ + actionName: 'Deploy', + input: sourceArtifact, + resource: s3.Bucket.fromBucketAttributes(stack, 'Bucket', { + bucketName: 'foo', + account: '1111', + }), + })); + }).toThrow(expectedError); + }); + + test('by declared account', () => { + // WHEN + expect(() => { + stage.addAction(new FakeBuildAction({ + actionName: 'Deploy', + input: sourceArtifact, account: '1111', - }), - })); - }).toThrow(expectedError); + })); + }).toThrow(expectedError); + }); }); - test('by declared account', () => { - // WHEN - expect(() => { + describe('also affects cross-region support stacks', () => { + let stage: codepipeline.IStage; + beforeEach(() => { + stage = pipeline.addStage({ stageName: 'Deploy' }); + }); + + test('when making a support stack', () => { + // WHEN stage.addAction(new FakeBuildAction({ actionName: 'Deploy', input: sourceArtifact, - account: '1111', + // No resource to grab onto forces creating a fresh support stack + region: 'eu-west-1', })); - }).toThrow(expectedError); - }); - }); - describe('also affects cross-region support stacks', () => { - let stage: codepipeline.IStage; - beforeEach(() => { - stage = pipeline.addStage({ stageName: 'Deploy' }); - }); - - test('when making a support stack', () => { - // WHEN - stage.addAction(new FakeBuildAction({ - actionName: 'Deploy', - input: sourceArtifact, - // No resource to grab onto forces creating a fresh support stack - region: 'eu-west-1', - })); - - // THEN - const asm = app.synth(); - const supportStack = asm.getStack(`${stack.stackName}-support-eu-west-1`); + // THEN + const asm = app.synth(); + const supportStack = asm.getStack(`${stack.stackName}-support-eu-west-1`); - // THEN - expect(supportStack).not.toHaveResource('AWS::KMS::Key'); - expect(supportStack).toHaveResource('AWS::S3::Bucket'); - }); + // THEN + expect(supportStack).not.toHaveResource('AWS::KMS::Key'); + expect(supportStack).toHaveResource('AWS::S3::Bucket'); + }); - test('when twiddling another stack', () => { - const stack2 = new Stack(app, 'Stack2', { env: { account: '2222', region: 'eu-west-1' } }); + test('when twiddling another stack', () => { + const stack2 = new Stack(app, 'Stack2', { env: { account: '2222', region: 'eu-west-1' } }); - // WHEN - stage.addAction(new FakeBuildAction({ - actionName: 'Deploy', - input: sourceArtifact, - resource: new iam.User(stack2, 'DoesntMatterWhatThisIs'), - })); + // WHEN + stage.addAction(new FakeBuildAction({ + actionName: 'Deploy', + input: sourceArtifact, + resource: new iam.User(stack2, 'DoesntMatterWhatThisIs'), + })); - // THEN - expect(stack2).not.toHaveResource('AWS::KMS::Key'); - expect(stack2).toHaveResource('AWS::S3::Bucket'); + // THEN + expect(stack2).not.toHaveResource('AWS::KMS::Key'); + expect(stack2).toHaveResource('AWS::S3::Bucket'); + }); }); }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts b/packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts index 880c45b1f8a05..3b091ac8ef339 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/pipeline.test.ts @@ -5,16 +5,15 @@ import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as codepipeline from '../lib'; import { FakeBuildAction } from './fake-build-action'; import { FakeSourceAction } from './fake-source-action'; /* eslint-disable quote-props */ -nodeunitShim({ - 'Pipeline': { - 'can be passed an IAM role during pipeline creation'(test: Test) { +describe('', () => { + describe('Pipeline', () => { + test('can be passed an IAM role during pipeline creation', () => { const stack = new cdk.Stack(); const role = new iam.Role(stack, 'Role', { assumedBy: new iam.ServicePrincipal('codepipeline.amazonaws.com'), @@ -32,23 +31,23 @@ nodeunitShim({ }, })); - test.done(); - }, - 'can be imported by ARN'(test: Test) { + }); + + test('can be imported by ARN', () => { const stack = new cdk.Stack(); const pipeline = codepipeline.Pipeline.fromPipelineArn(stack, 'Pipeline', 'arn:aws:codepipeline:us-east-1:123456789012:MyPipeline'); - test.equal(pipeline.pipelineArn, 'arn:aws:codepipeline:us-east-1:123456789012:MyPipeline'); - test.equal(pipeline.pipelineName, 'MyPipeline'); + expect(pipeline.pipelineArn).toEqual('arn:aws:codepipeline:us-east-1:123456789012:MyPipeline'); + expect(pipeline.pipelineName).toEqual('MyPipeline'); + - test.done(); - }, + }); - 'that is cross-region': { - 'validates that source actions are in the same region as the pipeline'(test: Test) { + describe('that is cross-region', () => { + test('validates that source actions are in the same region as the pipeline', () => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'PipelineStack', { env: { region: 'us-west-1', account: '123456789012' } }); const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); @@ -61,14 +60,14 @@ nodeunitShim({ region: 'ap-southeast-1', }); - test.throws(() => { + expect(() => { sourceStage.addAction(sourceAction); - }, /Source action 'FakeSource' must be in the same region as the pipeline/); + }).toThrow(/Source action 'FakeSource' must be in the same region as the pipeline/); + - test.done(); - }, + }); - 'allows passing an Alias in place of the KMS Key in the replication Bucket'(test: Test) { + test('allows passing an Alias in place of the KMS Key in the replication Bucket', () => { const app = new cdk.App(); const replicationRegion = 'us-west-1'; @@ -173,10 +172,10 @@ nodeunitShim({ }, }); - test.done(); - }, - "generates ArtifactStores with the alias' ARN as the KeyID"(test: Test) { + }); + + test('generates ArtifactStores with the alias ARN as the KeyID', () => { const app = new cdk.App(); const replicationRegion = 'us-west-1'; @@ -239,10 +238,10 @@ nodeunitShim({ 'UpdateReplacePolicy': 'Delete', }, ResourcePart.CompleteDefinition); - test.done(); - }, - 'allows passing an imported Bucket and Key for the replication Bucket'(test: Test) { + }); + + test('allows passing an imported Bucket and Key for the replication Bucket', () => { const replicationRegion = 'us-west-1'; const pipelineRegion = 'us-west-2'; @@ -296,10 +295,10 @@ nodeunitShim({ ], }); - test.done(); - }, - 'generates the support stack containing the replication Bucket without the need to bootstrap in that environment'(test: Test) { + }); + + test('generates the support stack containing the replication Bucket without the need to bootstrap in that environment', () => { const app = new cdk.App({ treeMetadata: false, // we can't set the context otherwise, because App will have a child }); @@ -331,17 +330,17 @@ nodeunitShim({ const assembly = app.synth(); const supportStackArtifact = assembly.getStackByName('PipelineStack-support-eu-south-1'); - test.equal(supportStackArtifact.assumeRoleArn, + expect(supportStackArtifact.assumeRoleArn).toEqual( 'arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-deploy-role-123456789012-us-west-2'); - test.equal(supportStackArtifact.cloudFormationExecutionRoleArn, + expect(supportStackArtifact.cloudFormationExecutionRoleArn).toEqual( 'arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-cfn-exec-role-123456789012-us-west-2'); - test.done(); - }, - }, - 'that is cross-account': { - 'does not allow passing a dynamic value in the Action account property'(test: Test) { + }); + }); + + describe('that is cross-account', () => { + test('does not allow passing a dynamic value in the Action account property', () => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'PipelineStack', { env: { account: '123456789012' } }); const sourceOutput = new codepipeline.Artifact(); @@ -355,18 +354,18 @@ nodeunitShim({ }); const buildStage = pipeline.addStage({ stageName: 'Build' }); - test.throws(() => { + expect(() => { buildStage.addAction(new FakeBuildAction({ actionName: 'FakeBuild', input: sourceOutput, account: cdk.Aws.ACCOUNT_ID, })); - }, /The 'account' property must be a concrete value \(action: 'FakeBuild'\)/); + }).toThrow(/The 'account' property must be a concrete value \(action: 'FakeBuild'\)/); + - test.done(); - }, + }); - 'does not allow an env-agnostic Pipeline Stack if an Action account has been provided'(test: Test) { + test('does not allow an env-agnostic Pipeline Stack if an Action account has been provided', () => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'PipelineStack'); const sourceOutput = new codepipeline.Artifact(); @@ -380,18 +379,18 @@ nodeunitShim({ }); const buildStage = pipeline.addStage({ stageName: 'Build' }); - test.throws(() => { + expect(() => { buildStage.addAction(new FakeBuildAction({ actionName: 'FakeBuild', input: sourceOutput, account: '123456789012', })); - }, /Pipeline stack which uses cross-environment actions must have an explicitly set account/); + }).toThrow(/Pipeline stack which uses cross-environment actions must have an explicitly set account/); + - test.done(); - }, - }, - }, + }); + }); + }); }); describe('test with shared setup', () => { 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 index 27ee88777d885..be155fca69a6d 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts @@ -38,17 +38,17 @@ export class ProviderAttribute { 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 */ + /** The phone number 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 */ + /** The picture attribute provided by Google */ public static readonly GOOGLE_PICTURE = new ProviderAttribute('picture'); - /** The email attribute provided by Google */ + /** The given name attribute provided by Google */ public static readonly GOOGLE_GIVEN_NAME = new ProviderAttribute('given_name'); - /** The email attribute provided by Google */ + /** The family name attribute provided by Google */ public static readonly GOOGLE_FAMILY_NAME = new ProviderAttribute('family_name'); /** @@ -195,4 +195,4 @@ export interface UserPoolIdentityProviderProps { * @default - no attribute mapping */ readonly attributeMapping?: AttributeMapping; -} \ 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 960d14bb8b426..d85199ee6507f 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -723,7 +723,7 @@ export class UserPool extends UserPoolBase { emailSubject: props.userInvitation?.emailSubject, smsMessage: props.userInvitation?.smsMessage, }; - const selfSignUpEnabled = props.selfSignUpEnabled !== undefined ? props.selfSignUpEnabled : false; + const selfSignUpEnabled = props.selfSignUpEnabled ?? false; const adminCreateUserConfig: CfnUserPool.AdminCreateUserConfigProperty = { allowAdminCreateUserOnly: !selfSignUpEnabled, inviteMessageTemplate: props.userInvitation !== undefined ? inviteMessageTemplate : undefined, 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 2dc031452ab61..0b3e525b4b74b 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 @@ -29,7 +29,7 @@ "devDependencies": { "aws-sdk": "^2.596.0", "aws-sdk-mock": "^5.1.0", - "eslint": "^7.13.0", + "eslint": "^7.19.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", @@ -37,6 +37,6 @@ "eslint-plugin-standard": "^4.1.0", "jest": "^26.6.3", "lambda-tester": "^3.6.0", - "nock": "^13.0.5" + "nock": "^13.0.7" } } diff --git a/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts b/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts index 45182672e45a0..718c0e693e454 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts @@ -1,10 +1,14 @@ import * as path from 'path'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Construct as CoreConstruct, Duration, NestedStack, Stack } from '@aws-cdk/core'; +import { Duration, NestedStack, Stack } from '@aws-cdk/core'; import * as cr from '@aws-cdk/custom-resources'; import { Construct } from 'constructs'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + export class ReplicaProvider extends NestedStack { /** * Creates a stack-singleton resource provider nested stack. diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 792f68f4cca3e..1b12eef42de1f 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -3,7 +3,7 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; 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, + Aws, CfnCondition, CfnCustomResource, CustomResource, Fn, IResource, Lazy, Names, RemovalPolicy, Resource, Stack, Token, } from '@aws-cdk/core'; import { Construct } from 'constructs'; @@ -14,6 +14,10 @@ import { ReplicaProvider } from './replica-provider'; import { EnableScalingProps, IScalableTableAttribute } from './scalable-attribute-api'; import { ScalableTableAttribute } from './scalable-table-attribute'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + const HASH_KEY_TYPE = 'HASH'; const RANGE_KEY_TYPE = 'RANGE'; @@ -1387,8 +1391,8 @@ export class Table extends TableBase { } return { - projectionType: props.projectionType ? props.projectionType : ProjectionType.ALL, - nonKeyAttributes: props.nonKeyAttributes ? props.nonKeyAttributes : undefined, + projectionType: props.projectionType ?? ProjectionType.ALL, + nonKeyAttributes: props.nonKeyAttributes ?? undefined, }; } diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index 6bae1adb77c14..e45cd7a017e4d 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -72,16 +72,16 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/jest": "^26.0.15", - "aws-sdk": "^2.830.0", + "@types/jest": "^26.0.20", + "aws-sdk": "^2.842.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.3", "pkglint": "0.0.0", - "sinon": "^9.2.1", - "ts-jest": "^26.4.4" + "sinon": "^9.2.4", + "ts-jest": "^26.5.1" }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index ae77a3024adbd..30dae02e8a0df 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -1,9 +1,10 @@ -import { ABSENT, ResourcePart, SynthUtils } from '@aws-cdk/assert'; +import { arrayWith, ABSENT, ResourcePart, SynthUtils } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { App, CfnDeletionPolicy, ConstructNode, Duration, PhysicalName, RemovalPolicy, Stack, Tags } from '@aws-cdk/core'; +import { App, Aws, CfnDeletionPolicy, ConstructNode, Duration, PhysicalName, RemovalPolicy, Stack, Tags } from '@aws-cdk/core'; +import { testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import { Attribute, AttributeType, @@ -486,154 +487,10 @@ test('fails if both replication regions used with customer managed CMK', () => { })).toThrow('TableEncryption.CUSTOMER_MANAGED is not supported by DynamoDB Global Tables (where replicationRegions was set)'); }); -test('if an encryption key is included, decrypt permissions are also added for grantStream', () => { - const stack = new Stack(); - const encryptionKey = new kms.Key(stack, 'Key', { - enableKeyRotation: true, - }); - const table = new Table(stack, 'Table A', { - tableName: TABLE_NAME, - partitionKey: TABLE_PARTITION_KEY, - encryptionKey, - stream: StreamViewType.NEW_IMAGE, - }); - const user = new iam.User(stack, 'MyUser'); - table.grantStreamRead(user); - expect(stack).toMatchTemplate({ - 'Resources': { - 'Key961B73FD': { - 'Type': 'AWS::KMS::Key', - 'Properties': { - 'KeyPolicy': { - 'Statement': [ - { - 'Action': [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', - ], - 'Effect': 'Allow', - 'Principal': { - 'AWS': { - 'Fn::Join': [ - '', - [ - 'arn:', - { - 'Ref': 'AWS::Partition', - }, - ':iam::', - { - 'Ref': 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - 'Resource': '*', - }, - ], - 'Version': '2012-10-17', - }, - 'EnableKeyRotation': true, - }, - 'UpdateReplacePolicy': 'Retain', - 'DeletionPolicy': 'Retain', - }, - 'TableA3D7B5AFA': { - 'Type': 'AWS::DynamoDB::Table', - 'Properties': { - 'KeySchema': [ - { - 'AttributeName': 'hashKey', - 'KeyType': 'HASH', - }, - ], - 'AttributeDefinitions': [ - { - 'AttributeName': 'hashKey', - 'AttributeType': 'S', - }, - ], - 'ProvisionedThroughput': { - 'ReadCapacityUnits': 5, - 'WriteCapacityUnits': 5, - }, - 'SSESpecification': { - 'KMSMasterKeyId': { - 'Fn::GetAtt': [ - 'Key961B73FD', - 'Arn', - ], - }, - 'SSEEnabled': true, - 'SSEType': 'KMS', - }, - 'StreamSpecification': { - 'StreamViewType': 'NEW_IMAGE', - }, - 'TableName': 'MyTable', - }, - 'UpdateReplacePolicy': 'Retain', - 'DeletionPolicy': 'Retain', - }, - 'MyUserDC45028B': { - 'Type': 'AWS::IAM::User', - }, - 'MyUserDefaultPolicy7B897426': { - 'Type': 'AWS::IAM::Policy', - 'Properties': { - 'PolicyDocument': { - 'Statement': [ - { - 'Action': 'dynamodb:ListStreams', - 'Effect': 'Allow', - 'Resource': '*', - }, - { - 'Action': [ - 'dynamodb:DescribeStream', - 'dynamodb:GetRecords', - 'dynamodb:GetShardIterator', - ], - 'Effect': 'Allow', - 'Resource': { - 'Fn::GetAtt': [ - 'TableA3D7B5AFA', - 'StreamArn', - ], - }, - }, - ], - 'Version': '2012-10-17', - }, - 'PolicyName': 'MyUserDefaultPolicy7B897426', - 'Users': [ - { - 'Ref': 'MyUserDC45028B', - }, - ], - }, - }, - }, - }); -}); - -test('if an encryption key is included, encrypt/decrypt permissions are also added both ways', () => { - const stack = new Stack(); +// this behaviour is only applicable without the future flag 'aws-kms:defaultKeyPolicies' +// see subsequent test for the updated behaviour +testLegacyBehavior('if an encryption key is included, encrypt/decrypt permissions are also added both ways', App, (app) => { + const stack = new Stack(app); const table = new Table(stack, 'Table A', { tableName: TABLE_NAME, partitionKey: TABLE_PARTITION_KEY, @@ -815,6 +672,38 @@ test('if an encryption key is included, encrypt/decrypt permissions are also add }); }); +test('if an encryption key is included, encrypt/decrypt permissions are added to the principal', () => { + const stack = new Stack(); + const table = new Table(stack, 'Table A', { + tableName: TABLE_NAME, + partitionKey: TABLE_PARTITION_KEY, + encryption: TableEncryption.CUSTOMER_MANAGED, + }); + const user = new iam.User(stack, 'MyUser'); + table.grantReadWriteData(user); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': arrayWith({ + 'Action': [ + 'kms:Decrypt', + 'kms:DescribeKey', + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': [ + 'TableAKey07CC09EC', + 'Arn', + ], + }, + }), + }, + }); +}); + test('when specifying PAY_PER_REQUEST billing mode', () => { const stack = new Stack(); new Table(stack, CONSTRUCT_NAME, { @@ -2244,7 +2133,7 @@ describe('import', () => { 'Roles': [{ 'Ref': 'NewRole99763075' }], }); - expect(table.tableArn).toBe('arn:${Token[AWS.Partition.3]}:dynamodb:${Token[AWS.Region.4]}:${Token[AWS.AccountId.0]}:table/MyTable'); + expect(table.tableArn).toBe(`arn:${Aws.PARTITION}:dynamodb:${Aws.REGION}:${Aws.ACCOUNT_ID}:table/MyTable`); expect(stack.resolve(table.tableName)).toBe(tableName); }); diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 6ab25bfb78b3f..3b902e5cb8c75 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -538,6 +538,9 @@ examples of things you might want to use: > `cdk.context.json`, or use the `cdk context` command. For more information, see > [Runtime Context](https://docs.aws.amazon.com/cdk/latest/guide/context.html) in the CDK > developer guide. +> +> `MachineImage.genericLinux()`, `MachineImage.genericWindows()` will use `CfnMapping` in +> an agnostic stack. ## Special VPC configurations @@ -999,3 +1002,24 @@ const subnet = Subnet.fromSubnetAttributes(this, 'SubnetFromAttributes', { // Supply only subnet id const subnet = Subnet.fromSubnetId(this, 'SubnetFromId', 's-1234'); ``` + +## Launch Templates + +A Launch Template is a standardized template that contains the configuration information to launch an instance. +They can be used when launching instances on their own, through Amazon EC2 Auto Scaling, EC2 Fleet, and Spot Fleet. +Launch templates enable you to store launch parameters so that you do not have to specify them every time you launch +an instance. For information on Launch Templates please see the +[official documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-launch-templates.html). + +The following demonstrates how to create a launch template with an Amazon Machine Image, and security group. + +```ts +const vpc = new ec2.Vpc(...); +// ... +const template = new ec2.LaunchTemplate(this, 'LaunchTemplate', { + machineImage: new ec2.AmazonMachineImage(), + securityGroup: new ec2.SecurityGroup(this, 'LaunchTemplateSG', { + vpc: vpc, + }), +}); +``` diff --git a/packages/@aws-cdk/aws-ec2/lib/bastion-host.ts b/packages/@aws-cdk/aws-ec2/lib/bastion-host.ts index dbc6c45fe415a..0656440ea3ff4 100644 --- a/packages/@aws-cdk/aws-ec2/lib/bastion-host.ts +++ b/packages/@aws-cdk/aws-ec2/lib/bastion-host.ts @@ -1,10 +1,10 @@ import { IPrincipal, IRole, PolicyStatement } from '@aws-cdk/aws-iam'; import { CfnOutput, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { AmazonLinuxGeneration, InstanceClass, InstanceSize, InstanceType } from '.'; +import { AmazonLinuxGeneration, InstanceArchitecture, InstanceClass, InstanceSize, InstanceType } from '.'; import { Connections } from './connections'; import { IInstance, Instance } from './instance'; -import { IMachineImage, MachineImage } from './machine-image'; +import { AmazonLinuxCpuType, IMachineImage, MachineImage } from './machine-image'; import { IPeer } from './peer'; import { Port } from './port'; import { ISecurityGroup } from './security-group'; @@ -60,10 +60,10 @@ export interface BastionHostLinuxProps { readonly instanceType?: InstanceType; /** - * The machine image to use + * The machine image to use, assumed to have SSM Agent preinstalled. * * @default - An Amazon Linux 2 image which is kept up-to-date automatically (the instance - * may be replaced on every deployment). + * may be replaced on every deployment) and already has SSM Agent installed. */ readonly machineImage?: IMachineImage; @@ -146,14 +146,17 @@ export class BastionHostLinux extends Resource implements IInstance { constructor(scope: Construct, id: string, props: BastionHostLinuxProps) { super(scope, id); this.stack = Stack.of(scope); - + const instanceType = props.instanceType ?? InstanceType.of(InstanceClass.T3, InstanceSize.NANO); this.instance = new Instance(this, 'Resource', { vpc: props.vpc, availabilityZone: props.availabilityZone, securityGroup: props.securityGroup, instanceName: props.instanceName ?? 'BastionHost', - instanceType: props.instanceType ?? InstanceType.of(InstanceClass.T3, InstanceSize.NANO), - machineImage: props.machineImage ?? MachineImage.latestAmazonLinux({ generation: AmazonLinuxGeneration.AMAZON_LINUX_2 }), + instanceType, + machineImage: props.machineImage ?? MachineImage.latestAmazonLinux({ + generation: AmazonLinuxGeneration.AMAZON_LINUX_2, + cpuType: this.toAmazonLinuxCpuType(instanceType.architecture), + }), vpcSubnets: props.subnetSelection ?? {}, blockDevices: props.blockDevices ?? undefined, }); @@ -165,8 +168,6 @@ export class BastionHostLinux extends Resource implements IInstance { ], resources: ['*'], })); - this.instance.addUserData('yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm'); - this.connections = this.instance.connections; this.role = this.instance.role; this.grantPrincipal = this.instance.role; @@ -183,6 +184,20 @@ export class BastionHostLinux extends Resource implements IInstance { }); } + /** + * Returns the AmazonLinuxCpuType corresponding to the given instance architecture + * @param architecture the instance architecture value to convert + */ + private toAmazonLinuxCpuType(architecture: InstanceArchitecture): AmazonLinuxCpuType { + if (architecture === InstanceArchitecture.ARM_64) { + return AmazonLinuxCpuType.ARM_64; + } else if (architecture === InstanceArchitecture.X86_64) { + return AmazonLinuxCpuType.X86_64; + } + + throw new Error(`Unsupported instance architecture '${architecture}'`); + } + /** * Allow SSH access from the given peer or peers * diff --git a/packages/@aws-cdk/aws-ec2/lib/cfn-init.ts b/packages/@aws-cdk/aws-ec2/lib/cfn-init.ts index 324847845849b..2388c80736df4 100644 --- a/packages/@aws-cdk/aws-ec2/lib/cfn-init.ts +++ b/packages/@aws-cdk/aws-ec2/lib/cfn-init.ts @@ -1,11 +1,15 @@ import * as crypto from 'crypto'; import * as iam from '@aws-cdk/aws-iam'; -import { Aws, CfnResource, Construct } from '@aws-cdk/core'; +import { Aws, CfnResource } from '@aws-cdk/core'; import { InitElement } from './cfn-init-elements'; import { OperatingSystemType } from './machine-image'; import { InitBindOptions, InitElementConfig, InitElementType, InitPlatform } from './private/cfn-init-internal'; import { UserData } from './user-data'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * A CloudFormation-init configuration */ diff --git a/packages/@aws-cdk/aws-ec2/lib/index.ts b/packages/@aws-cdk/aws-ec2/lib/index.ts index ca25a02f3f8d1..9f70a4320060d 100644 --- a/packages/@aws-cdk/aws-ec2/lib/index.ts +++ b/packages/@aws-cdk/aws-ec2/lib/index.ts @@ -4,6 +4,7 @@ export * from './cfn-init'; export * from './cfn-init-elements'; export * from './instance-types'; export * from './instance'; +export * from './launch-template'; export * from './machine-image'; export * from './nat'; export * from './network-acl'; diff --git a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts index be2a58561b181..c9a7f8ac74509 100644 --- a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts +++ b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts @@ -473,6 +473,21 @@ export enum InstanceClass { INF1 = 'inf1' } +/** + * Identifies an instance's CPU architecture + */ +export enum InstanceArchitecture { + /** + * ARM64 architecture + */ + ARM_64 = 'arm64', + + /** + * x86-64 architecture + */ + X86_64 = 'x86_64', +} + /** * What size of instance to use */ @@ -597,4 +612,26 @@ export class InstanceType { public toString(): string { return this.instanceTypeIdentifier; } + + /** + * The instance's CPU architecture + */ + public get architecture(): InstanceArchitecture { + // capture the family, generation, capabilities, and size portions of the instance type id + const instanceTypeComponents = this.instanceTypeIdentifier.match(/^([a-z]+)(\d{1,2})([a-z]*)\.([a-z0-9]+)$/); + if (instanceTypeComponents == null) { + throw new Error('Malformed instance type identifier'); + } + + const family = instanceTypeComponents[1]; + const capabilities = instanceTypeComponents[3]; + + // Instance family `a` are first-gen Graviton instances + // Capability `g` indicates the instance is Graviton2 powered + if (family === 'a' || capabilities.includes('g')) { + return InstanceArchitecture.ARM_64; + } + + return InstanceArchitecture.X86_64; + } } diff --git a/packages/@aws-cdk/aws-ec2/lib/instance.ts b/packages/@aws-cdk/aws-ec2/lib/instance.ts index 22c4fa7cf880f..82fbb22bea4e3 100644 --- a/packages/@aws-cdk/aws-ec2/lib/instance.ts +++ b/packages/@aws-cdk/aws-ec2/lib/instance.ts @@ -8,9 +8,10 @@ import { Connections, IConnectable } from './connections'; import { CfnInstance } from './ec2.generated'; import { InstanceType } from './instance-types'; import { IMachineImage, OperatingSystemType } from './machine-image'; +import { instanceBlockDeviceMappings } from './private/ebs-util'; import { ISecurityGroup, SecurityGroup } from './security-group'; import { UserData } from './user-data'; -import { BlockDevice, synthesizeBlockDeviceMappings } from './volume'; +import { BlockDevice } from './volume'; import { IVpc, Subnet, SubnetSelection } from './vpc'; /** @@ -362,7 +363,7 @@ export class Instance extends Resource implements IInstance { subnetId: subnet.subnetId, availabilityZone: subnet.availabilityZone, sourceDestCheck: props.sourceDestCheck, - blockDeviceMappings: props.blockDevices !== undefined ? synthesizeBlockDeviceMappings(this, props.blockDevices) : undefined, + blockDeviceMappings: props.blockDevices !== undefined ? instanceBlockDeviceMappings(this, props.blockDevices) : undefined, privateIpAddress: props.privateIpAddress, }); this.instance.node.addDependency(this.role); diff --git a/packages/@aws-cdk/aws-ec2/lib/launch-template.ts b/packages/@aws-cdk/aws-ec2/lib/launch-template.ts new file mode 100644 index 0000000000000..3b5b39f9b6370 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/lib/launch-template.ts @@ -0,0 +1,665 @@ +import * as iam from '@aws-cdk/aws-iam'; + +import { + Annotations, + Duration, + Expiration, + Fn, + IResource, + Lazy, + Resource, + TagManager, + TagType, + Tags, + Token, +} from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { Connections, IConnectable } from './connections'; +import { CfnLaunchTemplate } from './ec2.generated'; +import { InstanceType } from './instance-types'; +import { IMachineImage, MachineImageConfig, OperatingSystemType } from './machine-image'; +import { launchTemplateBlockDeviceMappings } from './private/ebs-util'; +import { ISecurityGroup } from './security-group'; +import { UserData } from './user-data'; +import { BlockDevice } from './volume'; + +/** + * Name tag constant + */ +const NAME_TAG: string = 'Name'; + +/** + * Provides the options for specifying the CPU credit type for burstable EC2 instance types (T2, T3, T3a, etc). + * + * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/burstable-performance-instances-how-to.html + */ +// dev-note: This could be used in the Instance L2 +export enum CpuCredits { + /** + * Standard bursting mode. + * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/burstable-performance-instances-standard-mode.html + */ + STANDARD = 'standard', + + /** + * Unlimited bursting mode. + * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/burstable-performance-instances-unlimited-mode.html + */ + UNLIMITED = 'unlimited', +}; + +/** + * Provides the options for specifying the instance initiated shutdown behavior. + * + * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/terminating-instances.html#Using_ChangingInstanceInitiatedShutdownBehavior + */ +// dev-note: This could be used in the Instance L2 +export enum InstanceInitiatedShutdownBehavior { + /** + * The instance will stop when it initiates a shutdown. + */ + STOP = 'stop', + + /** + * The instance will be terminated when it initiates a shutdown. + */ + TERMINATE = 'terminate', +}; + +/** + * Interface for LaunchTemplate-like objects. + */ +export interface ILaunchTemplate extends IResource { + /** + * The version number of this launch template to use + * + * @attribute + */ + readonly versionNumber: string; + + /** + * The identifier of the Launch Template + * + * Exactly one of `launchTemplateId` and `launchTemplateName` will be set. + * + * @attribute + */ + readonly launchTemplateId?: string; + + /** + * The name of the Launch Template + * + * Exactly one of `launchTemplateId` and `launchTemplateName` will be set. + * + * @attribute + */ + readonly launchTemplateName?: string; +} + +/** + * Provides the options for the types of interruption for spot instances. + */ +// dev-note: This could be used in a SpotFleet L2 if one gets developed. +export enum SpotInstanceInterruption { + /** + * The instance will stop when interrupted. + */ + STOP = 'stop', + + /** + * The instance will be terminated when interrupted. + */ + TERMINATE = 'terminate', + + /** + * The instance will hibernate when interrupted. + */ + HIBERNATE = 'hibernate', +} + +/** + * The Spot Instance request type. + * + * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-requests.html + */ +export enum SpotRequestType { + /** + * A one-time Spot Instance request remains active until Amazon EC2 launches the Spot Instance, + * the request expires, or you cancel the request. If the Spot price exceeds your maximum price + * or capacity is not available, your Spot Instance is terminated and the Spot Instance request + * is closed. + */ + ONE_TIME = 'one-time', + + /** + * A persistent Spot Instance request remains active until it expires or you cancel it, even if + * the request is fulfilled. If the Spot price exceeds your maximum price or capacity is not available, + * your Spot Instance is interrupted. After your instance is interrupted, when your maximum price exceeds + * the Spot price or capacity becomes available again, the Spot Instance is started if stopped or resumed + * if hibernated. + */ + PERSISTENT = 'persistent', +} + +/** + * Interface for the Spot market instance options provided in a LaunchTemplate. + */ +export interface LaunchTemplateSpotOptions { + /** + * Spot Instances with a defined duration (also known as Spot blocks) are designed not to be interrupted and will run continuously for the duration you select. + * You can use a duration of 1, 2, 3, 4, 5, or 6 hours. + * + * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-requests.html#fixed-duration-spot-instances + * + * @default Requested spot instances do not have a pre-defined duration. + */ + readonly blockDuration?: Duration; + + /** + * The behavior when a Spot Instance is interrupted. + * + * @default Spot instances will terminate when interrupted. + */ + readonly interruptionBehavior?: SpotInstanceInterruption; + + /** + * Maximum hourly price you're willing to pay for each Spot instance. The value is given + * in dollars. ex: 0.01 for 1 cent per hour, or 0.001 for one-tenth of a cent per hour. + * + * @default Maximum hourly price will default to the on-demand price for the instance type. + */ + readonly maxPrice?: number; + + /** + * The Spot Instance request type. + * + * If you are using Spot Instances with an Auto Scaling group, use one-time requests, as the + * Amazon EC2 Auto Scaling service handles requesting new Spot Instances whenever the group is + * below its desired capacity. + * + * @default One-time spot request. + */ + readonly requestType?: SpotRequestType; + + /** + * The end date of the request. For a one-time request, the request remains active until all instances + * launch, the request is canceled, or this date is reached. If the request is persistent, it remains + * active until it is canceled or this date and time is reached. + * + * @default The default end date is 7 days from the current date. + */ + readonly validUntil?: Expiration; +}; + +/** + * Properties of a LaunchTemplate. + */ +export interface LaunchTemplateProps { + /** + * Name for this launch template. + * + * @default Automatically generated name + */ + readonly launchTemplateName?: string; + + /** + * Type of instance to launch. + * + * @default - This Launch Template does not specify a default Instance Type. + */ + readonly instanceType?: InstanceType; + + /** + * The AMI that will be used by instances. + * + * @default - This Launch Template does not specify a default AMI. + */ + readonly machineImage?: IMachineImage; + + /** + * The AMI that will be used by instances. + * + * @default - This Launch Template creates a UserData based on the type of provided + * machineImage; no UserData is created if a machineImage is not provided + */ + readonly userData?: UserData; + + /** + * An IAM role to associate with the instance profile that is used by instances. + * + * The role must be assumable by the service principal `ec2.amazonaws.com`: + * + * @example + * const role = new iam.Role(this, 'MyRole', { + * assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') + * }); + * + * @default - No new role is created. + */ + readonly role?: iam.IRole; + + /** + * Specifies how block devices are exposed to the instance. You can specify virtual devices and EBS volumes. + * + * Each instance that is launched has an associated root device volume, + * either an Amazon EBS volume or an instance store volume. + * You can use block device mappings to specify additional EBS volumes or + * instance store volumes to attach to an instance when it is launched. + * + * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html + * + * @default - Uses the block device mapping of the AMI + */ + readonly blockDevices?: BlockDevice[]; + + /** + * CPU credit type for burstable EC2 instance types. + * + * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/burstable-performance-instances.html + * + * @default - No credit type is specified in the Launch Template. + */ + readonly cpuCredits?: CpuCredits; + + /** + * If you set this parameter to true, you cannot terminate the instances launched with this launch template + * using the Amazon EC2 console, CLI, or API; otherwise, you can. + * + * @default - The API termination setting is not specified in the Launch Template. + */ + readonly disableApiTermination?: boolean; + + /** + * Indicates whether the instances are optimized for Amazon EBS I/O. This optimization provides dedicated throughput + * to Amazon EBS and an optimized configuration stack to provide optimal Amazon EBS I/O performance. This optimization + * isn't available with all instance types. Additional usage charges apply when using an EBS-optimized instance. + * + * @default - EBS optimization is not specified in the launch template. + */ + readonly ebsOptimized?: boolean; + + /** + * If this parameter is set to true, the instance is enabled for AWS Nitro Enclaves; otherwise, it is not enabled for AWS Nitro Enclaves. + * + * @default - Enablement of Nitro enclaves is not specified in the launch template; defaulting to false. + */ + readonly nitroEnclaveEnabled?: boolean; + + /** + * If you set this parameter to true, the instance is enabled for hibernation. + * + * @default - Hibernation configuration is not specified in the launch template; defaulting to false. + */ + readonly hibernationConfigured?: boolean; + + /** + * Indicates whether an instance stops or terminates when you initiate shutdown from the instance (using the operating system command for system shutdown). + * + * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/terminating-instances.html#Using_ChangingInstanceInitiatedShutdownBehavior + * + * @default - Shutdown behavior is not specified in the launch template; defaults to STOP. + */ + readonly instanceInitiatedShutdownBehavior?: InstanceInitiatedShutdownBehavior; + + /** + * If this property is defined, then the Launch Template's InstanceMarketOptions will be + * set to use Spot instances, and the options for the Spot instances will be as defined. + * + * @default - Instance launched with this template will not be spot instances. + */ + readonly spotOptions?: LaunchTemplateSpotOptions; + + /** + * Name of SSH keypair to grant access to instance + * + * @default - No SSH access will be possible. + */ + readonly keyName?: string; + + /** + * If set to true, then detailed monitoring will be enabled on instances created with this + * launch template. + * + * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-cloudwatch-new.html + * + * @default False - Detailed monitoring is disabled. + */ + readonly detailedMonitoring?: boolean; + + /** + * Security group to assign to instances created with the launch template. + * + * @default No security group is assigned. + */ + readonly securityGroup?: ISecurityGroup; +} + +/** + * A class that provides convenient access to special version tokens for LaunchTemplate + * versions. + */ +export class LaunchTemplateSpecialVersions { + /** + * The special value that denotes that users of a Launch Template should + * reference the LATEST version of the template. + */ + public static readonly LATEST_VERSION: string = '$Latest'; + + /** + * The special value that denotes that users of a Launch Template should + * reference the DEFAULT version of the template. + */ + public static readonly DEFAULT_VERSION: string = '$Default'; +} + +/** + * Attributes for an imported LaunchTemplate. + */ +export interface LaunchTemplateAttributes { + /** + * The version number of this launch template to use + * + * @default Version: "$Default" + */ + readonly versionNumber?: string; + + /** + * The identifier of the Launch Template + * + * Exactly one of `launchTemplateId` and `launchTemplateName` may be set. + * + * @default None + */ + readonly launchTemplateId?: string; + + /** + * The name of the Launch Template + * + * Exactly one of `launchTemplateId` and `launchTemplateName` may be set. + * + * @default None + */ + readonly launchTemplateName?: string; +} + +/** + * This represents an EC2 LaunchTemplate. + * + * @see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-launch-templates.html + */ +export class LaunchTemplate extends Resource implements ILaunchTemplate, iam.IGrantable, IConnectable { + /** + * Import an existing LaunchTemplate. + */ + public static fromLaunchTemplateAttributes(scope: Construct, id: string, attrs: LaunchTemplateAttributes): ILaunchTemplate { + const haveId = Boolean(attrs.launchTemplateId); + const haveName = Boolean(attrs.launchTemplateName); + if (haveId == haveName) { + throw new Error('LaunchTemplate.fromLaunchTemplateAttributes() requires exactly one of launchTemplateId or launchTemplateName be provided.'); + } + + class Import extends Resource implements ILaunchTemplate { + public readonly versionNumber = attrs.versionNumber ?? LaunchTemplateSpecialVersions.DEFAULT_VERSION; + public readonly launchTemplateId? = attrs.launchTemplateId; + public readonly launchTemplateName? = attrs.launchTemplateName; + } + return new Import(scope, id); + } + + // ============================================ + // Members for ILaunchTemplate interface + + public readonly versionNumber: string; + public readonly launchTemplateId?: string; + public readonly launchTemplateName?: string; + + // ============================================= + // Data members + + /** + * The default version for the launch template. + * + * @attribute + */ + public readonly defaultVersionNumber: string; + + /** + * The latest version of the launch template. + * + * @attribute + */ + public readonly latestVersionNumber: string; + + /** + * The type of OS the instance is running. + * + * @attribute + */ + public readonly osType?: OperatingSystemType; + + /** + * IAM Role assumed by instances that are launched from this template. + * + * @attribute + */ + public readonly role?: iam.IRole; + + /** + * UserData executed by instances that are launched from this template. + * + * @attribute + */ + public readonly userData?: UserData; + + // ============================================= + // Private/protected data members + + /** + * Principal to grant permissions to. + * @internal + */ + protected readonly _grantPrincipal?: iam.IPrincipal; + + /** + * Allows specifying security group connections for the instance. + * @internal + */ + protected readonly _connections?: Connections; + + /** + * TagManager for tagging support. + */ + protected readonly tags: TagManager; + + // ============================================= + + constructor(scope: Construct, id: string, props: LaunchTemplateProps = {}) { + super(scope, id); + + // Basic validation of the provided spot block duration + const spotDuration = props?.spotOptions?.blockDuration?.toHours({ integral: true }); + if (spotDuration !== undefined && (spotDuration < 1 || spotDuration > 6)) { + // See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-requests.html#fixed-duration-spot-instances + Annotations.of(this).addError('Spot block duration must be exactly 1, 2, 3, 4, 5, or 6 hours.'); + } + + this.role = props.role; + this._grantPrincipal = this.role; + const iamProfile: iam.CfnInstanceProfile | undefined = this.role ? new iam.CfnInstanceProfile(this, 'Profile', { + roles: [this.role!.roleName], + }) : undefined; + + if (props.securityGroup) { + this._connections = new Connections({ securityGroups: [props.securityGroup] }); + } + const securityGroupsToken = Lazy.list({ + produce: () => { + if (this._connections && this._connections.securityGroups.length > 0) { + return this._connections.securityGroups.map(sg => sg.securityGroupId); + } + return undefined; + }, + }); + + if (props.userData) { + this.userData = props.userData; + } + const userDataToken = Lazy.string({ + produce: () => { + if (this.userData) { + return Fn.base64(this.userData.render()); + } + return undefined; + }, + }); + + const imageConfig: MachineImageConfig | undefined = props.machineImage?.getImage(this); + if (imageConfig) { + this.osType = imageConfig.osType; + } + + let marketOptions: any = undefined; + if (props?.spotOptions) { + marketOptions = { + marketType: 'spot', + spotOptions: { + blockDurationMinutes: spotDuration !== undefined ? spotDuration * 60 : undefined, + instanceInterruptionBehavior: props.spotOptions.interruptionBehavior, + maxPrice: props.spotOptions.maxPrice?.toString(), + spotInstanceType: props.spotOptions.requestType, + validUntil: props.spotOptions.validUntil?.date.toUTCString(), + }, + }; + // Remove SpotOptions if there are none. + if (Object.keys(marketOptions.spotOptions).filter(k => marketOptions.spotOptions[k]).length == 0) { + marketOptions.spotOptions = undefined; + } + } + + this.tags = new TagManager(TagType.KEY_VALUE, 'AWS::EC2::LaunchTemplate'); + const tagsToken = Lazy.any({ + produce: () => { + if (this.tags.hasTags()) { + const renderedTags = this.tags.renderTags(); + const lowerCaseRenderedTags = renderedTags.map( (tag: { [key: string]: string}) => { + return { + key: tag.Key, + value: tag.Value, + }; + }); + return [ + { + resourceType: 'instance', + tags: lowerCaseRenderedTags, + }, + { + resourceType: 'volume', + tags: lowerCaseRenderedTags, + }, + ]; + } + return undefined; + }, + }); + + const resource = new CfnLaunchTemplate(this, 'Resource', { + launchTemplateName: props?.launchTemplateName, + launchTemplateData: { + blockDeviceMappings: props?.blockDevices !== undefined ? launchTemplateBlockDeviceMappings(this, props.blockDevices) : undefined, + creditSpecification: props?.cpuCredits !== undefined ? { + cpuCredits: props.cpuCredits, + } : undefined, + disableApiTermination: props?.disableApiTermination, + ebsOptimized: props?.ebsOptimized, + enclaveOptions: props?.nitroEnclaveEnabled !== undefined ? { + enabled: props.nitroEnclaveEnabled, + } : undefined, + hibernationOptions: props?.hibernationConfigured !== undefined ? { + configured: props.hibernationConfigured, + } : undefined, + iamInstanceProfile: iamProfile !== undefined ? { + arn: iamProfile.getAtt('Arn').toString(), + } : undefined, + imageId: imageConfig?.imageId, + instanceType: props?.instanceType?.toString(), + instanceInitiatedShutdownBehavior: props?.instanceInitiatedShutdownBehavior, + instanceMarketOptions: marketOptions, + keyName: props?.keyName, + monitoring: props?.detailedMonitoring !== undefined ? { + enabled: props.detailedMonitoring, + } : undefined, + securityGroupIds: securityGroupsToken, + tagSpecifications: tagsToken, + userData: userDataToken, + + // Fields not yet implemented: + // ========================== + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-capacityreservationspecification.html + // Will require creating an L2 for AWS::EC2::CapacityReservation + // capacityReservationSpecification: undefined, + + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-cpuoptions.html + // cpuOptions: undefined, + + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-elasticgpuspecification.html + // elasticGpuSpecifications: undefined, + + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata.html#cfn-ec2-launchtemplate-launchtemplatedata-elasticinferenceaccelerators + // elasticInferenceAccelerators: undefined, + + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata.html#cfn-ec2-launchtemplate-launchtemplatedata-kernelid + // kernelId: undefined, + // ramDiskId: undefined, + + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata.html#cfn-ec2-launchtemplate-launchtemplatedata-licensespecifications + // Also not implemented in Instance L2 + // licenseSpecifications: undefined, + + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata.html#cfn-ec2-launchtemplate-launchtemplatedata-metadataoptions + // metadataOptions: undefined, + + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata.html#cfn-ec2-launchtemplate-launchtemplatedata-tagspecifications + // Should be implemented via the Tagging aspect in CDK core. Complication will be that this tagging interface is very unique to LaunchTemplates. + // tagSpecification: undefined + + // CDK has no abstraction for Network Interfaces yet. + // networkInterfaces: undefined, + + // CDK has no abstraction for Placement yet. + // placement: undefined, + + }, + }); + + Tags.of(this).add(NAME_TAG, this.node.path); + + this.defaultVersionNumber = resource.attrDefaultVersionNumber; + this.latestVersionNumber = resource.attrLatestVersionNumber; + this.launchTemplateId = resource.ref; + this.versionNumber = Token.asString(resource.getAtt('LatestVersionNumber')); + } + + /** + * Allows specifying security group connections for the instance. + * + * @note Only available if you provide a securityGroup when constructing the LaunchTemplate. + */ + public get connections(): Connections { + if (!this._connections) { + throw new Error('LaunchTemplate can only be used as IConnectable if a securityGroup is provided when contructing it.'); + } + return this._connections; + } + + /** + * Principal to grant permissions to. + * + * @note Only available if you provide a role when constructing the LaunchTemplate. + */ + public get grantPrincipal(): iam.IPrincipal { + if (!this._grantPrincipal) { + throw new Error('LaunchTemplate can only be used as IGrantable if a role is provided when constructing it.'); + } + return this._grantPrincipal; + } +} diff --git a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts index 34405181774a1..df4a1eece07e0 100644 --- a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts +++ b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts @@ -1,10 +1,14 @@ import * as ssm from '@aws-cdk/aws-ssm'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import { Construct, ContextProvider, Stack, Token } from '@aws-cdk/core'; +import { ContextProvider, CfnMapping, Aws, Stack, Token } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { UserData } from './user-data'; import { WindowsVersion } from './windows-versions'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Interface for classes that can select an appropriate machine image to use */ @@ -363,24 +367,33 @@ export interface GenericWindowsImageProps { * manually specify an AMI map. */ export class GenericLinuxImage implements IMachineImage { - constructor(private readonly amiMap: {[region: string]: string}, private readonly props: GenericLinuxImageProps = {}) { + constructor(private readonly amiMap: { [region: string]: string }, private readonly props: GenericLinuxImageProps = {}) { } public getImage(scope: Construct): MachineImageConfig { + const userData = this.props.userData ?? UserData.forLinux(); + const osType = OperatingSystemType.LINUX; const region = Stack.of(scope).region; if (Token.isUnresolved(region)) { - throw new Error('Unable to determine AMI from AMI map since stack is region-agnostic'); + const mapping: { [k1: string]: { [k2: string]: any } } = {}; + for (const [rgn, ami] of Object.entries(this.amiMap)) { + mapping[rgn] = { ami }; + } + const amiMap = new CfnMapping(scope, 'AmiMap', { mapping }); + return { + imageId: amiMap.findInMap(Aws.REGION, 'ami'), + userData, + osType, + }; } - - const ami = region !== 'test-region' ? this.amiMap[region] : 'ami-12345'; - if (!ami) { + const imageId = region !== 'test-region' ? this.amiMap[region] : 'ami-12345'; + if (!imageId) { throw new Error(`Unable to find AMI in AMI map: no AMI specified for region '${region}'`); } - return { - imageId: ami, - userData: this.props.userData ?? UserData.forLinux(), - osType: OperatingSystemType.LINUX, + imageId, + userData, + osType, }; } } @@ -395,20 +408,29 @@ export class GenericWindowsImage implements IMachineImage { } public getImage(scope: Construct): MachineImageConfig { + const userData = this.props.userData ?? UserData.forWindows(); + const osType = OperatingSystemType.WINDOWS; const region = Stack.of(scope).region; if (Token.isUnresolved(region)) { - throw new Error('Unable to determine AMI from AMI map since stack is region-agnostic'); + const mapping: { [k1: string]: { [k2: string]: any } } = {}; + for (const [rgn, ami] of Object.entries(this.amiMap)) { + mapping[rgn] = { ami }; + } + const amiMap = new CfnMapping(scope, 'AmiMap', { mapping }); + return { + imageId: amiMap.findInMap(Aws.REGION, 'ami'), + userData, + osType, + }; } - - const ami = region !== 'test-region' ? this.amiMap[region] : 'ami-12345'; - if (!ami) { + const imageId = region !== 'test-region' ? this.amiMap[region] : 'ami-12345'; + if (!imageId) { throw new Error(`Unable to find AMI in AMI map: no AMI specified for region '${region}'`); } - return { - imageId: ami, - userData: this.props.userData ?? UserData.forWindows(), - osType: OperatingSystemType.WINDOWS, + imageId, + userData, + osType, }; } } diff --git a/packages/@aws-cdk/aws-ec2/lib/network-acl.ts b/packages/@aws-cdk/aws-ec2/lib/network-acl.ts index bea1b030d2c9a..911948e8df39c 100644 --- a/packages/@aws-cdk/aws-ec2/lib/network-acl.ts +++ b/packages/@aws-cdk/aws-ec2/lib/network-acl.ts @@ -277,7 +277,7 @@ export class NetworkAclEntry extends NetworkAclEntryBase { new CfnNetworkAclEntry(this, 'Resource', { networkAclId: this.networkAcl.networkAclId, ruleNumber: props.ruleNumber, - ruleAction: props.ruleAction !== undefined ? props.ruleAction : Action.ALLOW, + ruleAction: props.ruleAction ?? Action.ALLOW, egress: props.direction !== undefined ? props.direction === TrafficDirection.EGRESS : undefined, ...props.traffic.toTrafficConfig(), ...props.cidr.toCidrConfig(), diff --git a/packages/@aws-cdk/aws-ec2/lib/private/ebs-util.ts b/packages/@aws-cdk/aws-ec2/lib/private/ebs-util.ts new file mode 100644 index 0000000000000..dc91f6d795011 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/lib/private/ebs-util.ts @@ -0,0 +1,42 @@ +import { Annotations } from '@aws-cdk/core'; +import { CfnInstance, CfnLaunchTemplate } from '../ec2.generated'; +import { BlockDevice, EbsDeviceVolumeType } from '../volume'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +export function instanceBlockDeviceMappings(construct: Construct, blockDevices: BlockDevice[]): CfnInstance.BlockDeviceMappingProperty[] { + return synthesizeBlockDeviceMappings(construct, blockDevices, {}); +} + +export function launchTemplateBlockDeviceMappings(construct: Construct, blockDevices: BlockDevice[]): CfnLaunchTemplate.BlockDeviceMappingProperty[] { + return synthesizeBlockDeviceMappings(construct, blockDevices, ''); +} + +/** + * Synthesize an array of block device mappings from a list of block device + * + * @param construct the instance/asg construct, used to host any warning + * @param blockDevices list of block devices + */ +function synthesizeBlockDeviceMappings(construct: Construct, blockDevices: BlockDevice[], noDeviceValue: NDT): RT[] { + return blockDevices.map(({ deviceName, volume, mappingEnabled }): RT => { + const { virtualName, ebsDevice: ebs } = volume; + + if (ebs) { + const { iops, volumeType } = ebs; + + if (!iops) { + if (volumeType === EbsDeviceVolumeType.IO1) { + throw new Error('iops property is required with volumeType: EbsDeviceVolumeType.IO1'); + } + } else if (volumeType !== EbsDeviceVolumeType.IO1) { + Annotations.of(construct).addWarning('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); + } + } + + const noDevice = mappingEnabled === false ? noDeviceValue : undefined; + return { deviceName, ebs, virtualName, noDevice } as any; + }); +} diff --git a/packages/@aws-cdk/aws-ec2/lib/user-data.ts b/packages/@aws-cdk/aws-ec2/lib/user-data.ts index 21727212948c0..20061bd609636 100644 --- a/packages/@aws-cdk/aws-ec2/lib/user-data.ts +++ b/packages/@aws-cdk/aws-ec2/lib/user-data.ts @@ -147,7 +147,7 @@ class LinuxUserData extends UserData { } public render(): string { - const shebang = this.props.shebang !== undefined ? this.props.shebang : '#!/bin/bash'; + const shebang = this.props.shebang ?? '#!/bin/bash'; return [shebang, ...(this.renderOnExitLines()), ...this.lines].join('\n'); } diff --git a/packages/@aws-cdk/aws-ec2/lib/volume.ts b/packages/@aws-cdk/aws-ec2/lib/volume.ts index 65d84b30eed55..a23614b89ed63 100644 --- a/packages/@aws-cdk/aws-ec2/lib/volume.ts +++ b/packages/@aws-cdk/aws-ec2/lib/volume.ts @@ -2,9 +2,9 @@ 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, Names } from '@aws-cdk/core'; +import { IResource, Resource, Size, SizeRoundingBehavior, Stack, Token, Tags, Names } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { CfnInstance, CfnVolume } from './ec2.generated'; +import { CfnVolume } from './ec2.generated'; import { IInstance } from './instance'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. @@ -164,37 +164,6 @@ export class BlockDeviceVolume { } } -/** - * Synthesize an array of block device mappings from a list of block device - * - * @param construct the instance/asg construct, used to host any warning - * @param blockDevices list of block devices - */ -export function synthesizeBlockDeviceMappings(construct: Construct, blockDevices: BlockDevice[]): CfnInstance.BlockDeviceMappingProperty[] { - return blockDevices.map(({ deviceName, volume, mappingEnabled }) => { - const { virtualName, ebsDevice: ebs } = volume; - - if (ebs) { - const { iops, volumeType } = ebs; - - if (!iops) { - if (volumeType === EbsDeviceVolumeType.IO1) { - throw new Error('iops property is required with volumeType: EbsDeviceVolumeType.IO1'); - } - } else if (volumeType !== EbsDeviceVolumeType.IO1) { - Annotations.of(construct).addWarning('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); - } - } - - return { - deviceName, - ebs, - virtualName, - noDevice: mappingEnabled === false ? {} : undefined, - }; - }); -} - /** * Supported EBS volume types for blockDevices */ @@ -371,7 +340,7 @@ export interface VolumeProps { /** * The size of the volume, in GiBs. You must specify either a snapshot ID or a volume size. - * See {@link https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html#ebs-volume-characteristics|Volume Characteristics} + * See {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html} * for details on the allowable size for each type of volume. * * @default If you're creating the volume from a snapshot and don't specify a volume size, the default is the snapshot size. @@ -452,13 +421,14 @@ export interface VolumeProps { readonly volumeType?: EbsDeviceVolumeType; /** - * The number of I/O operations per second (IOPS) to provision for the volume, with a maximum ratio of 50 IOPS/GiB. - * See {@link https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html#EBSVolumeTypes_piops|Provisioned IOPS SSD (io1) volumes} + * The number of I/O operations per second (IOPS) to provision for the volume. The maximum ratio is 50 IOPS/GiB for PROVISIONED_IOPS_SSD, + * and 500 IOPS/GiB for both PROVISIONED_IOPS_SSD_IO2 and GENERAL_PURPOSE_SSD_GP3. + * See {@link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html} * for more information. * - * This parameter is valid only for PROVISIONED_IOPS_SSD volumes. + * This parameter is valid only for PROVISIONED_IOPS_SSD, PROVISIONED_IOPS_SSD_IO2 and GENERAL_PURPOSE_SSD_GP3 volumes. * - * @default None -- Required for {@link EbsDeviceVolumeType.PROVISIONED_IOPS_SSD} + * @default None -- Required for io1 and io2 volumes. The default for gp3 volumes is 3,000 IOPS if omitted. */ readonly iops?: number; } @@ -673,34 +643,79 @@ export class Volume extends VolumeBase { throw new Error('`encrypted` must be true when providing an `encryptionKey`.'); } + if ( + props.volumeType && + [ + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, + ].includes(props.volumeType) && + !props.iops + ) { + throw new Error( + '`iops` must be specified if the `volumeType` is `PROVISIONED_IOPS_SSD` or `PROVISIONED_IOPS_SSD_IO2`.', + ); + } + if (props.iops) { - if (props.volumeType !== EbsDeviceVolumeType.PROVISIONED_IOPS_SSD) { - throw new Error('`iops` may only be specified if the `volumeType` is `PROVISIONED_IOPS_SSD`/`IO1`'); + const volumeType = props.volumeType ?? EbsDeviceVolumeType.GENERAL_PURPOSE_SSD; + if ( + ![ + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, + EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, + ].includes(volumeType) + ) { + throw new Error( + '`iops` may only be specified if the `volumeType` is `PROVISIONED_IOPS_SSD`, `PROVISIONED_IOPS_SSD_IO2` or `GENERAL_PURPOSE_SSD_GP3`.', + ); } - - if (props.iops < 100 || props.iops > 64000) { - throw new Error('`iops` must be in the range 100 to 64,000, inclusive.'); + // Enforce minimum & maximum IOPS: + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html + const iopsRanges: { [key: string]: { Min: number, Max: number } } = {}; + iopsRanges[EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3] = { Min: 3000, Max: 16000 }; + iopsRanges[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD] = { Min: 100, Max: 64000 }; + iopsRanges[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2] = { Min: 100, Max: 64000 }; + const { Min, Max } = iopsRanges[volumeType]; + if (props.iops < Min || props.iops > Max) { + throw new Error(`\`${volumeType}\` volumes iops must be between ${Min} and ${Max}.`); } - if (props.size && (props.iops > 50 * props.size.toGibibytes({ rounding: SizeRoundingBehavior.FAIL }))) { - throw new Error('`iops` has a maximum ratio of 50 IOPS/GiB.'); + // Enforce maximum ratio of IOPS/GiB: + // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html + const maximumRatios: { [key: string]: number } = {}; + maximumRatios[EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3] = 500; + maximumRatios[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD] = 50; + maximumRatios[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2] = 500; + const maximumRatio = maximumRatios[volumeType]; + if (props.size && (props.iops > maximumRatio * props.size.toGibibytes({ rounding: SizeRoundingBehavior.FAIL }))) { + throw new Error(`\`${volumeType}\` volumes iops has a maximum ratio of ${maximumRatio} IOPS/GiB.`); } } - if (props.enableMultiAttach && props.volumeType !== EbsDeviceVolumeType.PROVISIONED_IOPS_SSD) { - throw new Error('multi-attach is supported exclusively on `PROVISIONED_IOPS_SSD` volumes.'); + if (props.enableMultiAttach) { + const volumeType = props.volumeType ?? EbsDeviceVolumeType.GENERAL_PURPOSE_SSD; + if ( + ![ + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, + ].includes(volumeType) + ) { + throw new Error('multi-attach is supported exclusively on `PROVISIONED_IOPS_SSD` and `PROVISIONED_IOPS_SSD_IO2` volumes.'); + } } if (props.size) { const size = props.size.toGibibytes({ rounding: SizeRoundingBehavior.FAIL }); - // Enforce maximum volume size: - // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html#ebs-volume-characteristics + // Enforce minimum & maximum volume size: + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-ebs-volume.html const sizeRanges: { [key: string]: { Min: number, Max: number } } = {}; - sizeRanges[EbsDeviceVolumeType.GENERAL_PURPOSE_SSD] = { Min: 1, Max: 16000 }; - sizeRanges[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD] = { Min: 4, Max: 16000 }; - sizeRanges[EbsDeviceVolumeType.THROUGHPUT_OPTIMIZED_HDD] = { Min: 500, Max: 16000 }; - sizeRanges[EbsDeviceVolumeType.COLD_HDD] = { Min: 500, Max: 16000 }; - sizeRanges[EbsDeviceVolumeType.MAGNETIC] = { Min: 1, Max: 1000 }; + sizeRanges[EbsDeviceVolumeType.GENERAL_PURPOSE_SSD] = { Min: 1, Max: 16384 }; + sizeRanges[EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3] = { Min: 1, Max: 16384 }; + sizeRanges[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD] = { Min: 4, Max: 16384 }; + sizeRanges[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2] = { Min: 4, Max: 16384 }; + sizeRanges[EbsDeviceVolumeType.THROUGHPUT_OPTIMIZED_HDD] = { Min: 125, Max: 16384 }; + sizeRanges[EbsDeviceVolumeType.COLD_HDD] = { Min: 125, Max: 16384 }; + sizeRanges[EbsDeviceVolumeType.MAGNETIC] = { Min: 1, Max: 1024 }; const volumeType = props.volumeType ?? EbsDeviceVolumeType.GENERAL_PURPOSE_SSD; const { Min, Max } = sizeRanges[volumeType]; if (size < Min || size > Max) { diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts index d17e56aab202c..87f67391f8c9b 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint-service.ts @@ -86,8 +86,8 @@ export class VpcEndpointService extends Resource implements IVpcEndpointService } this.vpcEndpointServiceLoadBalancers = props.vpcEndpointServiceLoadBalancers; - this.acceptanceRequired = props.acceptanceRequired !== undefined ? props.acceptanceRequired : true; - this.whitelistedPrincipals = props.whitelistedPrincipals !== undefined ? props.whitelistedPrincipals : []; + this.acceptanceRequired = props.acceptanceRequired ?? true; + this.whitelistedPrincipals = props.whitelistedPrincipals ?? []; this.endpointService = new CfnVPCEndpointService(this, id, { networkLoadBalancerArns: this.vpcEndpointServiceLoadBalancers.map(lb => lb.loadBalancerArn), @@ -98,8 +98,7 @@ export class VpcEndpointService extends Resource implements IVpcEndpointService const { region } = Stack.of(this); const serviceNamePrefix = !Token.isUnresolved(region) ? - RegionInfo.get(region).vpcEndpointServiceNamePrefix ?? - Default.VPC_ENDPOINT_SERVICE_NAME_PREFIX : + (RegionInfo.get(region).vpcEndpointServiceNamePrefix ?? Default.VPC_ENDPOINT_SERVICE_NAME_PREFIX) : Default.VPC_ENDPOINT_SERVICE_NAME_PREFIX; this.vpcEndpointServiceName = Fn.join('.', [serviceNamePrefix, Aws.REGION, this.vpcEndpointServiceId]); diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index d23433b2637d8..2a4e524fc5150 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -1213,7 +1213,7 @@ export class Vpc extends VpcBase { this.availabilityZones = stack.availabilityZones; - const maxAZs = props.maxAzs !== undefined ? props.maxAzs : 3; + const maxAZs = props.maxAzs ?? 3; this.availabilityZones = this.availabilityZones.slice(0, maxAZs); this.vpcId = this.resource.ref; @@ -1788,7 +1788,7 @@ export class PrivateSubnet extends Subnet implements IPrivateSubnet { } function ifUndefined(value: T | undefined, defaultValue: T): T { - return value !== undefined ? value : defaultValue; + return value ?? defaultValue; } class ImportedVpc extends VpcBase { @@ -1886,6 +1886,7 @@ class LookedUpVpc extends VpcBase { availabilityZone: vpcSubnet.availabilityZone, subnetId: vpcSubnet.subnetId, routeTableId: vpcSubnet.routeTableId, + ipv4CidrBlock: vpcSubnet.cidr, })); } return ret; diff --git a/packages/@aws-cdk/aws-ec2/lib/vpn.ts b/packages/@aws-cdk/aws-ec2/lib/vpn.ts index 4683180d2af84..19fb4f963acd3 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpn.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpn.ts @@ -224,7 +224,7 @@ export class VpnConnection extends Resource implements IVpnConnection { }); } - if (!net.isIPv4(props.ip)) { + if (!Token.isUnresolved(props.ip) && !net.isIPv4(props.ip)) { throw new Error(`The \`ip\` ${props.ip} is not a valid IPv4 address.`); } diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 71bf6c111fab9..74ceca63c984f 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -95,7 +95,6 @@ }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { - "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", diff --git a/packages/@aws-cdk/aws-ec2/test/bastion-host.test.ts b/packages/@aws-cdk/aws-ec2/test/bastion-host.test.ts index e950a397707b8..6c547d7859c1a 100644 --- a/packages/@aws-cdk/aws-ec2/test/bastion-host.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/bastion-host.test.ts @@ -1,7 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import { Stack } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; -import { BastionHostLinux, BlockDeviceVolume, SubnetType, Vpc } from '../lib'; +import { BastionHostLinux, BlockDeviceVolume, InstanceClass, InstanceSize, InstanceType, SubnetType, Vpc } from '../lib'; nodeunitShim({ 'default instance is created in basic'(test: Test) { @@ -83,6 +83,45 @@ nodeunitShim({ ], })); + test.done(); + }, + 'x86-64 instances use x86-64 image by default'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = new Vpc(stack, 'VPC'); + + // WHEN + new BastionHostLinux(stack, 'Bastion', { + vpc, + }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::Instance', { + ImageId: { + Ref: 'SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter', + }, + })); + + test.done(); + }, + 'arm instances use arm image by default'(test: Test) { + // GIVEN + const stack = new Stack(); + const vpc = new Vpc(stack, 'VPC'); + + // WHEN + new BastionHostLinux(stack, 'Bastion', { + vpc, + instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.NANO), + }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::Instance', { + ImageId: { + Ref: 'SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmarm64gp2C96584B6F00A464EAD1953AFF4B05118Parameter', + }, + })); + test.done(); }, }); diff --git a/packages/@aws-cdk/aws-ec2/test/instance.test.ts b/packages/@aws-cdk/aws-ec2/test/instance.test.ts index 6d002e38af568..a2049fb31e86b 100644 --- a/packages/@aws-cdk/aws-ec2/test/instance.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/instance.test.ts @@ -8,7 +8,7 @@ import { Stack } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; import { AmazonLinuxImage, BlockDeviceVolume, CloudFormationInit, - EbsDeviceVolumeType, InitCommand, Instance, InstanceClass, InstanceSize, InstanceType, UserData, Vpc, + EbsDeviceVolumeType, InitCommand, Instance, InstanceArchitecture, InstanceClass, InstanceSize, InstanceType, UserData, Vpc, } from '../lib'; @@ -107,7 +107,56 @@ nodeunitShim({ test.done(); }, + 'instance architecture is correctly discerned for arm instances'(test: Test) { + // GIVEN + const sampleInstanceClasses = [ + 'a1', 't4g', 'c6g', 'c6gd', 'c6gn', 'm6g', 'm6gd', 'r6g', 'r6gd', // current Graviton-based instance classes + 'a13', 't11g', 'y10ng', 'z11ngd', // theoretical future Graviton-based instance classes + ]; + + for (const instanceClass of sampleInstanceClasses) { + // WHEN + const instanceType = InstanceType.of(instanceClass as InstanceClass, InstanceSize.XLARGE18); + + // THEN + expect(instanceType.architecture).toBe(InstanceArchitecture.ARM_64); + } + + test.done(); + }, + 'instance architecture is correctly discerned for x86-64 instance'(test: Test) { + // GIVEN + const sampleInstanceClasses = ['c5', 'm5ad', 'r5n', 'm6', 't3a']; // A sample of x86-64 instance classes + for (const instanceClass of sampleInstanceClasses) { + // WHEN + const instanceType = InstanceType.of(instanceClass as InstanceClass, InstanceSize.XLARGE18); + + // THEN + expect(instanceType.architecture).toBe(InstanceArchitecture.X86_64); + } + + test.done(); + }, + 'instance architecture throws an error when instance type is invalid'(test: Test) { + // GIVEN + const malformedInstanceTypes = ['t4', 't4g.nano.', 't4gnano', '']; + + for (const malformedInstanceType of malformedInstanceTypes) { + // WHEN + const instanceType = new InstanceType(malformedInstanceType); + + // THEN + try { + instanceType.architecture; + expect(true).toBe(false); // The line above should have thrown an error + } catch (err) { + expect(err.message).toBe('Malformed instance type identifier'); + } + } + + test.done(); + }, blockDeviceMappings: { 'can set blockDeviceMappings'(test: Test) { // WHEN diff --git a/packages/@aws-cdk/aws-ec2/test/integ.bastion-host-arm-support.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.bastion-host-arm-support.expected.json new file mode 100644 index 0000000000000..81f4ae3377d40 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.bastion-host-arm-support.expected.json @@ -0,0 +1,659 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/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": "TestStack/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/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": "TestStack/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/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": "TestStack/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/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": "TestStack/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/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": "TestStack/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTable98AE0E14": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/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": "TestStack/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3NATGatewayD3048F5C": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet3EIPAD4BC883", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/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": "TestStack/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/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": "TestStack/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/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": "TestStack/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTable192186F8": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStack/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": "TestStack/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "BastionHostInstanceSecurityGroupE75D4274": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "TestStack/BastionHost/Resource/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "BastionHost" + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "BastionHostInstanceRoleDD3FA5F1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "BastionHost" + } + ] + } + }, + "BastionHostInstanceRoleDefaultPolicy17347525": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ssmmessages:*", + "ssm:UpdateInstanceInformation", + "ec2messages:*" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "BastionHostInstanceRoleDefaultPolicy17347525", + "Roles": [ + { + "Ref": "BastionHostInstanceRoleDD3FA5F1" + } + ] + } + }, + "BastionHostInstanceProfile770FCA07": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "BastionHostInstanceRoleDD3FA5F1" + } + ] + } + }, + "BastionHost30F9ED05": { + "Type": "AWS::EC2::Instance", + "Properties": { + "AvailabilityZone": "test-region-1a", + "IamInstanceProfile": { + "Ref": "BastionHostInstanceProfile770FCA07" + }, + "ImageId": { + "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmarm64gp2C96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceType": "t4g.nano", + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "BastionHostInstanceSecurityGroupE75D4274", + "GroupId" + ] + } + ], + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + "Tags": [ + { + "Key": "Name", + "Value": "BastionHost" + } + ], + "UserData": { + "Fn::Base64": "#!/bin/bash" + } + }, + "DependsOn": [ + "BastionHostInstanceRoleDefaultPolicy17347525", + "BastionHostInstanceRoleDD3FA5F1" + ] + } + }, + "Outputs": { + "BastionHostBastionHostIdC743CBD6": { + "Description": "Instance ID of the bastion host. Use this to connect via SSM Session Manager", + "Value": { + "Ref": "BastionHost30F9ED05" + } + } + }, + "Parameters": { + "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmarm64gp2C96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-arm64-gp2" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.bastion-host-arm-support.ts b/packages/@aws-cdk/aws-ec2/test/integ.bastion-host-arm-support.ts new file mode 100644 index 0000000000000..06d6d12557ba9 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.bastion-host-arm-support.ts @@ -0,0 +1,26 @@ +/* + * Stack verification steps: + * * aws ssm start-session --target + * * lscpu # Architecture should be aarch64 + */ +import * as cdk from '@aws-cdk/core'; +import * as ec2 from '../lib'; + +const app = new cdk.App(); + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const vpc = new ec2.Vpc(this, 'VPC'); + + new ec2.BastionHostLinux(this, 'BastionHost', { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.NANO), + }); + } +} + +new TestStack(app, 'TestStack'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-ec2/test/integ.bastion-host.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.bastion-host.expected.json index 73b52ab630a76..4943873897e75 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.bastion-host.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.bastion-host.expected.json @@ -633,7 +633,7 @@ } ], "UserData": { - "Fn::Base64": "#!/bin/bash\nyum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm" + "Fn::Base64": "#!/bin/bash" } }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts b/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts new file mode 100644 index 0000000000000..882cd69ed282f --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/launch-template.test.ts @@ -0,0 +1,685 @@ +import { + countResources, + expect as expectCDK, + haveResource, + haveResourceLike, + stringLike, +} from '@aws-cdk/assert'; +import { + CfnInstanceProfile, + Role, + ServicePrincipal, +} from '@aws-cdk/aws-iam'; +import { + App, + Duration, + Expiration, + Stack, + Tags, +} from '@aws-cdk/core'; +import { + AmazonLinuxImage, + BlockDevice, + BlockDeviceVolume, + CpuCredits, + EbsDeviceVolumeType, + InstanceInitiatedShutdownBehavior, + InstanceType, + LaunchTemplate, + OperatingSystemType, + SecurityGroup, + SpotInstanceInterruption, + SpotRequestType, + UserData, + Vpc, + WindowsImage, + WindowsVersion, +} from '../lib'; + +/* eslint-disable jest/expect-expect */ + +describe('LaunchTemplate', () => { + let app: App; + let stack: Stack; + + beforeEach(() => { + app = new App(); + stack = new Stack(app); + }); + + test('Empty props', () => { + // WHEN + const template = new LaunchTemplate(stack, 'Template'); + + // THEN + // Note: The following is intentionally a haveResource instead of haveResourceLike + // to ensure that only the bare minimum of properties have values when no properties + // are given to a LaunchTemplate. + expectCDK(stack).to(haveResource('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + TagSpecifications: [ + { + ResourceType: 'instance', + Tags: [ + { + Key: 'Name', + Value: 'Default/Template', + }, + ], + }, + { + ResourceType: 'volume', + Tags: [ + { + Key: 'Name', + Value: 'Default/Template', + }, + ], + }, + ], + }, + })); + expectCDK(stack).notTo(haveResource('AWS::IAM::InstanceProfile')); + expect(() => { template.grantPrincipal; }).toThrow(); + expect(() => { template.connections; }).toThrow(); + expect(template.osType).toBeUndefined(); + expect(template.role).toBeUndefined(); + expect(template.userData).toBeUndefined(); + }); + + test('Import from attributes with name', () => { + // WHEN + const template = LaunchTemplate.fromLaunchTemplateAttributes(stack, 'Template', { + launchTemplateName: 'TestName', + versionNumber: 'TestVersion', + }); + + // THEN + expect(template.launchTemplateId).toBeUndefined(); + expect(template.launchTemplateName).toBe('TestName'); + expect(template.versionNumber).toBe('TestVersion'); + }); + + test('Import from attributes with id', () => { + // WHEN + const template = LaunchTemplate.fromLaunchTemplateAttributes(stack, 'Template', { + launchTemplateId: 'TestId', + versionNumber: 'TestVersion', + }); + + // THEN + expect(template.launchTemplateId).toBe('TestId'); + expect(template.launchTemplateName).toBeUndefined(); + expect(template.versionNumber).toBe('TestVersion'); + }); + + test('Import from attributes fails with name and id', () => { + expect(() => { + LaunchTemplate.fromLaunchTemplateAttributes(stack, 'Template', { + launchTemplateName: 'TestName', + launchTemplateId: 'TestId', + versionNumber: 'TestVersion', + }); + }).toThrow(); + }); + + test('Given name', () => { + // WHEN + new LaunchTemplate(stack, 'Template', { + launchTemplateName: 'LTName', + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateName: 'LTName', + })); + }); + + test('Given instanceType', () => { + // WHEN + new LaunchTemplate(stack, 'Template', { + instanceType: new InstanceType('tt.test'), + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + InstanceType: 'tt.test', + }, + })); + }); + + test('Given machineImage (Linux)', () => { + // WHEN + const template = new LaunchTemplate(stack, 'Template', { + machineImage: new AmazonLinuxImage(), + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + ImageId: { + Ref: stringLike('SsmParameterValueawsserviceamiamazonlinuxlatestamznami*Parameter'), + }, + }, + })); + expect(template.osType).toBe(OperatingSystemType.LINUX); + expect(template.userData).toBeUndefined(); + }); + + test('Given machineImage (Windows)', () => { + // WHEN + const template = new LaunchTemplate(stack, 'Template', { + machineImage: new WindowsImage(WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_BASE), + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + ImageId: { + Ref: stringLike('SsmParameterValueawsserviceamiwindowslatestWindowsServer2019EnglishFullBase*Parameter'), + }, + }, + })); + expect(template.osType).toBe(OperatingSystemType.WINDOWS); + expect(template.userData).toBeUndefined(); + }); + + test('Given userData', () => { + // GIVEN + const userData = UserData.forLinux(); + userData.addCommands('echo Test'); + + // WHEN + const template = new LaunchTemplate(stack, 'Template', { + userData, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + UserData: { + 'Fn::Base64': '#!/bin/bash\necho Test', + }, + }, + })); + expect(template.userData).toBeDefined(); + }); + + test('Given role', () => { + // GIVEN + const role = new Role(stack, 'TestRole', { + assumedBy: new ServicePrincipal('ec2.amazonaws.com'), + }); + + // WHEN + const template = new LaunchTemplate(stack, 'Template', { + role, + }); + + // THEN + expectCDK(stack).to(countResources('AWS::IAM::Role', 1)); + expectCDK(stack).to(haveResourceLike('AWS::IAM::InstanceProfile', { + Roles: [ + { + Ref: 'TestRole6C9272DF', + }, + ], + })); + expectCDK(stack).to(haveResource('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + IamInstanceProfile: { + Arn: stack.resolve((template.node.findChild('Profile') as CfnInstanceProfile).getAtt('Arn')), + }, + TagSpecifications: [ + { + ResourceType: 'instance', + Tags: [ + { + Key: 'Name', + Value: 'Default/Template', + }, + ], + }, + { + ResourceType: 'volume', + Tags: [ + { + Key: 'Name', + Value: 'Default/Template', + }, + ], + }, + ], + }, + })); + expect(template.role).toBeDefined(); + expect(template.grantPrincipal).toBeDefined(); + }); + + test('Given blockDeviceMapping', () => { + // GIVEN + const blockDevices: BlockDevice[] = [ + { + deviceName: 'ebs', + mappingEnabled: true, + volume: BlockDeviceVolume.ebs(15, { + deleteOnTermination: true, + encrypted: true, + volumeType: EbsDeviceVolumeType.IO1, + iops: 5000, + }), + }, { + deviceName: 'ebs-snapshot', + mappingEnabled: false, + volume: BlockDeviceVolume.ebsFromSnapshot('snapshot-id', { + volumeSize: 500, + deleteOnTermination: false, + volumeType: EbsDeviceVolumeType.SC1, + }), + }, { + deviceName: 'ephemeral', + volume: BlockDeviceVolume.ephemeral(0), + }, + ]; + + // WHEN + new LaunchTemplate(stack, 'Template', { + blockDevices, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + BlockDeviceMappings: [ + { + DeviceName: 'ebs', + Ebs: { + DeleteOnTermination: true, + Encrypted: true, + Iops: 5000, + VolumeSize: 15, + VolumeType: 'io1', + }, + }, + { + DeviceName: 'ebs-snapshot', + Ebs: { + DeleteOnTermination: false, + SnapshotId: 'snapshot-id', + VolumeSize: 500, + VolumeType: 'sc1', + }, + NoDevice: '', + }, + { + DeviceName: 'ephemeral', + VirtualName: 'ephemeral0', + }, + ], + }, + })); + }); + + test.each([ + [CpuCredits.STANDARD, 'standard'], + [CpuCredits.UNLIMITED, 'unlimited'], + ])('Given cpuCredits %p', (given: CpuCredits, expected: string) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + cpuCredits: given, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + CreditSpecification: { + CpuCredits: expected, + }, + }, + })); + }); + + test.each([ + [true, true], + [false, false], + ])('Given disableApiTermination %p', (given: boolean, expected: boolean) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + disableApiTermination: given, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + DisableApiTermination: expected, + }, + })); + }); + + test.each([ + [true, true], + [false, false], + ])('Given ebsOptimized %p', (given: boolean, expected: boolean) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + ebsOptimized: given, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + EbsOptimized: expected, + }, + })); + }); + + test.each([ + [true, true], + [false, false], + ])('Given nitroEnclaveEnabled %p', (given: boolean, expected: boolean) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + nitroEnclaveEnabled: given, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + EnclaveOptions: { + Enabled: expected, + }, + }, + })); + }); + + test.each([ + [InstanceInitiatedShutdownBehavior.STOP, 'stop'], + [InstanceInitiatedShutdownBehavior.TERMINATE, 'terminate'], + ])('Given instanceInitiatedShutdownBehavior %p', (given: InstanceInitiatedShutdownBehavior, expected: string) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + instanceInitiatedShutdownBehavior: given, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + InstanceInitiatedShutdownBehavior: expected, + }, + })); + }); + + test('Given keyName', () => { + // WHEN + new LaunchTemplate(stack, 'Template', { + keyName: 'TestKeyname', + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + KeyName: 'TestKeyname', + }, + })); + }); + + test.each([ + [true, true], + [false, false], + ])('Given detailedMonitoring %p', (given: boolean, expected: boolean) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + detailedMonitoring: given, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + Monitoring: { + Enabled: expected, + }, + }, + })); + }); + + test('Given securityGroup', () => { + // GIVEN + const vpc = new Vpc(stack, 'VPC'); + const sg = new SecurityGroup(stack, 'SG', { vpc }); + + // WHEN + const template = new LaunchTemplate(stack, 'Template', { + securityGroup: sg, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + SecurityGroupIds: [ + { + 'Fn::GetAtt': [ + 'SGADB53937', + 'GroupId', + ], + }, + ], + }, + })); + expect(template.connections).toBeDefined(); + expect(template.connections.securityGroups).toHaveLength(1); + expect(template.connections.securityGroups[0]).toBe(sg); + }); + + test('Adding tags', () => { + // GIVEN + const template = new LaunchTemplate(stack, 'Template'); + + // WHEN + Tags.of(template).add('TestKey', 'TestValue'); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + TagSpecifications: [ + { + ResourceType: 'instance', + Tags: [ + { + Key: 'Name', + Value: 'Default/Template', + }, + { + Key: 'TestKey', + Value: 'TestValue', + }, + ], + }, + { + ResourceType: 'volume', + Tags: [ + { + Key: 'Name', + Value: 'Default/Template', + }, + { + Key: 'TestKey', + Value: 'TestValue', + }, + ], + }, + ], + }, + })); + }); +}); + +describe('LaunchTemplate marketOptions', () => { + let app: App; + let stack: Stack; + + beforeEach(() => { + app = new App(); + stack = new Stack(app); + }); + + test('given spotOptions', () => { + // WHEN + new LaunchTemplate(stack, 'Template', { + spotOptions: {}, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + InstanceMarketOptions: { + MarketType: 'spot', + }, + }, + })); + }); + + test.each([ + [0, 1], + [1, 0], + [6, 0], + [7, 1], + ])('for range duration errors: %p', (duration: number, expectedErrors: number) => { + // WHEN + const template = new LaunchTemplate(stack, 'Template', { + spotOptions: { + blockDuration: Duration.hours(duration), + }, + }); + + // THEN + expect(template.node.metadata).toHaveLength(expectedErrors); + }); + + test('for bad duration', () => { + expect(() => { + new LaunchTemplate(stack, 'Template', { + spotOptions: { + // Duration must be an integral number of hours. + blockDuration: Duration.minutes(61), + }, + }); + }).toThrow(); + }); + + test('given blockDuration', () => { + // WHEN + new LaunchTemplate(stack, 'Template', { + spotOptions: { + blockDuration: Duration.hours(1), + }, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + InstanceMarketOptions: { + MarketType: 'spot', + SpotOptions: { + BlockDurationMinutes: 60, + }, + }, + }, + })); + }); + + test.each([ + [SpotInstanceInterruption.STOP, 'stop'], + [SpotInstanceInterruption.TERMINATE, 'terminate'], + [SpotInstanceInterruption.HIBERNATE, 'hibernate'], + ])('given interruptionBehavior %p', (given: SpotInstanceInterruption, expected: string) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + spotOptions: { + interruptionBehavior: given, + }, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + InstanceMarketOptions: { + MarketType: 'spot', + SpotOptions: { + InstanceInterruptionBehavior: expected, + }, + }, + }, + })); + }); + + test.each([ + [0.001, '0.001'], + [1, '1'], + [2.5, '2.5'], + ])('given maxPrice %p', (given: number, expected: string) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + spotOptions: { + maxPrice: given, + }, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + InstanceMarketOptions: { + MarketType: 'spot', + SpotOptions: { + MaxPrice: expected, + }, + }, + }, + })); + }); + + test.each([ + [SpotRequestType.ONE_TIME, 'one-time'], + [SpotRequestType.PERSISTENT, 'persistent'], + ])('given requestType %p', (given: SpotRequestType, expected: string) => { + // WHEN + new LaunchTemplate(stack, 'Template', { + spotOptions: { + requestType: given, + }, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + InstanceMarketOptions: { + MarketType: 'spot', + SpotOptions: { + SpotInstanceType: expected, + }, + }, + }, + })); + }); + + test('given validUntil', () => { + // WHEN + new LaunchTemplate(stack, 'Template', { + spotOptions: { + validUntil: Expiration.atTimestamp(0), + }, + }); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', { + LaunchTemplateData: { + InstanceMarketOptions: { + MarketType: 'spot', + SpotOptions: { + ValidUntil: 'Thu, 01 Jan 1970 00:00:00 GMT', + }, + }, + }, + })); + }); +}); diff --git a/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts b/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts index 2fcf7e2980be9..43e6dbdcd88cf 100644 --- a/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/machine-image.test.ts @@ -1,3 +1,4 @@ +import { expect as cdkExpect, matchTemplate, MatchStyle } from '@aws-cdk/assert'; import { App, Stack } from '@aws-cdk/core'; import * as ec2 from '../lib'; @@ -11,6 +12,43 @@ beforeEach(() => { }); }); +test('can make and use a Linux image', () => { + // WHEN + const image = new ec2.GenericLinuxImage({ + testregion: 'ami-1234', + }); + + // THEN + const details = image.getImage(stack); + expect(details.imageId).toEqual('ami-1234'); + expect(details.osType).toEqual(ec2.OperatingSystemType.LINUX); +}); + +test('can make and use a Linux image in agnostic stack', () => { + // WHEN + app = new App(); + stack = new Stack(app, 'Stack'); + const image = new ec2.GenericLinuxImage({ + testregion: 'ami-1234', + }); + + // THEN + const details = image.getImage(stack); + const expected = { + Mappings: { + AmiMap: { + testregion: { + ami: 'ami-1234', + }, + }, + }, + }; + + cdkExpect(stack).to(matchTemplate(expected, MatchStyle.EXACT)); + expect(stack.resolve(details.imageId)).toEqual({ 'Fn::FindInMap': ['AmiMap', { Ref: 'AWS::Region' }, 'ami'] }); + expect(details.osType).toEqual(ec2.OperatingSystemType.LINUX); +}); + test('can make and use a Windows image', () => { // WHEN const image = new ec2.GenericWindowsImage({ @@ -23,6 +61,31 @@ test('can make and use a Windows image', () => { expect(details.osType).toEqual(ec2.OperatingSystemType.WINDOWS); }); +test('can make and use a Windows image in agnostic stack', () => { + // WHEN + app = new App(); + stack = new Stack(app, 'Stack'); + const image = new ec2.GenericWindowsImage({ + testregion: 'ami-1234', + }); + + // THEN + const details = image.getImage(stack); + const expected = { + Mappings: { + AmiMap: { + testregion: { + ami: 'ami-1234', + }, + }, + }, + }; + + cdkExpect(stack).to(matchTemplate(expected, MatchStyle.EXACT)); + expect(stack.resolve(details.imageId)).toEqual({ 'Fn::FindInMap': ['AmiMap', { Ref: 'AWS::Region' }, 'ami'] }); + expect(details.osType).toEqual(ec2.OperatingSystemType.WINDOWS); +}); + test('can make and use a Generic SSM image', () => { // WHEN const image = new ec2.GenericSSMParameterImage('testParam', ec2.OperatingSystemType.LINUX); diff --git a/packages/@aws-cdk/aws-ec2/test/userdata.test.ts b/packages/@aws-cdk/aws-ec2/test/userdata.test.ts index b674e72abac68..883794bd5c585 100644 --- a/packages/@aws-cdk/aws-ec2/test/userdata.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/userdata.test.ts @@ -1,6 +1,6 @@ import { Bucket } from '@aws-cdk/aws-s3'; +import { Aws, Stack } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; -import { Stack } from '../../core/lib'; import * as ec2 from '../lib'; nodeunitShim({ @@ -52,7 +52,7 @@ nodeunitShim({ test.equals(rendered, 'trap {\n' + '$success=($PSItem.Exception.Message -eq "Success")\n' + - 'cfn-signal --stack Default --resource RESOURCE1989552F --region ${Token[AWS.Region.4]} --success ($success.ToString().ToLower())\n' + + `cfn-signal --stack Default --resource RESOURCE1989552F --region ${Aws.REGION} --success ($success.ToString().ToLower())\n` + 'break\n' + '}\n' + 'command1\n' + @@ -157,7 +157,7 @@ nodeunitShim({ test.equals(rendered, '#!/bin/bash\n' + 'function exitTrap(){\n' + 'exitCode=$?\n' + - '/opt/aws/bin/cfn-signal --stack Default --resource RESOURCE1989552F --region ${Token[AWS.Region.4]} -e $exitCode || echo \'Failed to send Cloudformation Signal\'\n' + + `/opt/aws/bin/cfn-signal --stack Default --resource RESOURCE1989552F --region ${Aws.REGION} -e $exitCode || echo \'Failed to send Cloudformation Signal\'\n` + '}\n' + 'trap exitTrap EXIT\n' + 'command1'); diff --git a/packages/@aws-cdk/aws-ec2/test/volume.test.ts b/packages/@aws-cdk/aws-ec2/test/volume.test.ts index e0d37a232c6d6..4e5da04a38976 100644 --- a/packages/@aws-cdk/aws-ec2/test/volume.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/volume.test.ts @@ -1,4 +1,5 @@ import { + arrayWith, expect as cdkExpect, haveResource, haveResourceLike, @@ -10,7 +11,7 @@ import { } from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; +import { testFutureBehavior, testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import { AmazonLinuxGeneration, EbsDeviceVolumeType, @@ -21,8 +22,8 @@ import { Vpc, } from '../lib'; -nodeunitShim({ - 'basic volume'(test: Test) { +describe('volume', () => { + test('basic volume', () => { // GIVEN const stack = new cdk.Stack(); @@ -40,10 +41,10 @@ nodeunitShim({ VolumeType: 'gp2', }, ResourcePart.Properties)); - test.done(); - }, - 'fromVolumeAttributes'(test: Test) { + }); + + test('fromVolumeAttributes', () => { // GIVEN const stack = new cdk.Stack(); const encryptionKey = new kms.Key(stack, 'Key'); @@ -58,13 +59,13 @@ nodeunitShim({ }); // THEN - test.strictEqual(volume.volumeId, volumeId); - test.strictEqual(volume.availabilityZone, availabilityZone); - test.strictEqual(volume.encryptionKey, encryptionKey); - test.done(); - }, + expect(volume.volumeId).toEqual(volumeId); + expect(volume.availabilityZone).toEqual(availabilityZone); + expect(volume.encryptionKey).toEqual(encryptionKey); + + }); - 'tagged volume'(test: Test) { + test('tagged volume', () => { // GIVEN const stack = new cdk.Stack(); const volume = new Volume(stack, 'Volume', { @@ -87,10 +88,10 @@ nodeunitShim({ }], }, ResourcePart.Properties)); - test.done(); - }, - 'autoenableIO'(test: Test) { + }); + + test('autoenableIO', () => { // GIVEN const stack = new cdk.Stack(); @@ -106,10 +107,10 @@ nodeunitShim({ AutoEnableIO: true, }, ResourcePart.Properties)); - test.done(); - }, - 'encryption'(test: Test) { + }); + + test('encryption', () => { // GIVEN const stack = new cdk.Stack(); @@ -125,10 +126,10 @@ nodeunitShim({ Encrypted: true, }, ResourcePart.Properties)); - test.done(); - }, - 'encryption with kms'(test: Test) { + }); + + test('encryption with kms', () => { // GIVEN const stack = new cdk.Stack(); const encryptionKey = new kms.Key(stack, 'Key'); @@ -204,12 +205,14 @@ nodeunitShim({ }, })); - test.done(); - }, - 'encryption with kms from snapshot'(test: Test) { + }); + + // only enable for legacy behaviour + // see https://github.com/aws/aws-cdk/issues/12962 + testLegacyBehavior('encryption with kms from snapshot', cdk.App, (app) => { // GIVEN - const stack = new cdk.Stack(); + const stack = new cdk.Stack(app); const encryptionKey = new kms.Key(stack, 'Key'); // WHEN @@ -237,10 +240,10 @@ nodeunitShim({ }, })); - test.done(); - }, - 'iops'(test: Test) { + }); + + test('iops', () => { // GIVEN const stack = new cdk.Stack(); @@ -258,10 +261,10 @@ nodeunitShim({ VolumeType: 'io1', }, ResourcePart.Properties)); - test.done(); - }, - 'multi-attach'(test: Test) { + }); + + test('multi-attach', () => { // GIVEN const stack = new cdk.Stack(); @@ -279,10 +282,10 @@ nodeunitShim({ MultiAttachEnabled: true, }, ResourcePart.Properties)); - test.done(); - }, - 'snapshotId'(test: Test) { + }); + + test('snapshotId', () => { // GIVEN const stack = new cdk.Stack(); @@ -297,10 +300,10 @@ nodeunitShim({ SnapshotId: 'snap-00000000', }, ResourcePart.Properties)); - test.done(); - }, - 'volume: standard'(test: Test) { + }); + + test('volume: standard', () => { // GIVEN const stack = new cdk.Stack(); @@ -316,10 +319,10 @@ nodeunitShim({ VolumeType: 'standard', }, ResourcePart.Properties)); - test.done(); - }, - 'volume: io1'(test: Test) { + }); + + test('volume: io1', () => { // GIVEN const stack = new cdk.Stack(); @@ -328,6 +331,7 @@ nodeunitShim({ availabilityZone: 'us-east-1a', size: cdk.Size.gibibytes(500), volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + iops: 300, }); // THEN @@ -335,10 +339,30 @@ nodeunitShim({ VolumeType: 'io1', }, ResourcePart.Properties)); - test.done(); - }, - 'volume: gp2'(test: Test) { + }); + + test('volume: io2', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, + iops: 300, + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + VolumeType: 'io2', + }, ResourcePart.Properties)); + + + }); + + test('volume: gp2', () => { // GIVEN const stack = new cdk.Stack(); @@ -354,10 +378,29 @@ nodeunitShim({ VolumeType: 'gp2', }, ResourcePart.Properties)); - test.done(); - }, - 'volume: st1'(test: Test) { + }); + + test('volume: gp3', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + VolumeType: 'gp3', + }, ResourcePart.Properties)); + + + }); + + test('volume: st1', () => { // GIVEN const stack = new cdk.Stack(); @@ -373,10 +416,10 @@ nodeunitShim({ VolumeType: 'st1', }, ResourcePart.Properties)); - test.done(); - }, - 'volume: sc1'(test: Test) { + }); + + test('volume: sc1', () => { // GIVEN const stack = new cdk.Stack(); @@ -392,10 +435,10 @@ nodeunitShim({ VolumeType: 'sc1', }, ResourcePart.Properties)); - test.done(); - }, - 'grantAttachVolume to any instance'(test: Test) { + }); + + test('grantAttachVolume to any instance', () => { // GIVEN const stack = new cdk.Stack(); const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); @@ -462,40 +505,94 @@ nodeunitShim({ }], }, })); - test.done(); - }, - 'grantAttachVolume to any instance with encryption'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); - const encryptionKey = new kms.Key(stack, 'Key'); - const volume = new Volume(stack, 'Volume', { - availabilityZone: 'us-east-1a', - size: cdk.Size.gibibytes(8), - encrypted: true, - encryptionKey, - }); + }); - // WHEN - volume.grantAttachVolume(role); + describe('grantAttachVolume to any instance with encryption', () => { - // THEN - cdkExpect(stack).to(haveResourceLike('AWS::KMS::Key', { - KeyPolicy: { - Statement: [ - {}, - {}, - { - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::GetAtt': [ - 'Role1ABCC5F0', - 'Arn', - ], + // This exact assertions here are only applicable when 'aws-kms:defaultKeyPolicies' feature flag is disabled. + // See subsequent test case for the updated behaviour + testLegacyBehavior('legacy', cdk.App, (app) => { + // GIVEN + const stack = new cdk.Stack(app); + const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); + const encryptionKey = new kms.Key(stack, 'Key'); + const volume = new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + encrypted: true, + encryptionKey, + }); + + // WHEN + volume.grantAttachVolume(role); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + {}, + {}, + { + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], + }, + }, + Action: 'kms:CreateGrant', + Condition: { + Bool: { + 'kms:GrantIsForAWSResource': true, + }, + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'ec2.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', + ], + ], + }, + 'kms:GrantConstraintType': 'EncryptionContextSubset', + }, }, + Resource: '*', }, + ], + }, + })); + + + }); + + testFutureBehavior('with future flag aws-kms:defaultKeyPolicies', { '@aws-cdk/aws-kms:defaultKeyPolicies': true }, cdk.App, (app) => { + // GIVEN + const stack = new cdk.Stack(app); + const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); + const encryptionKey = new kms.Key(stack, 'Key'); + const volume = new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + encrypted: true, + encryptionKey, + }); + + // WHEN + volume.grantAttachVolume(role); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith({ + Effect: 'Allow', Action: 'kms:CreateGrant', Condition: { Bool: { @@ -517,16 +614,22 @@ nodeunitShim({ 'kms:GrantConstraintType': 'EncryptionContextSubset', }, }, - Resource: '*', - }, - ], - }, - })); + Resource: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + }), + }, + })); - test.done(); - }, - 'grantAttachVolume to any instance with KMS.fromKeyArn() encryption'(test: Test) { + }); + + }); + + test('grantAttachVolume to any instance with KMS.fromKeyArn() encryption', () => { // GIVEN const stack = new cdk.Stack(); const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); @@ -598,10 +701,10 @@ nodeunitShim({ }, })); - test.done(); - }, - 'grantAttachVolume to specific instances'(test: Test) { + }); + + test('grantAttachVolume to specific instances', () => { // GIVEN const stack = new cdk.Stack(); const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); @@ -686,10 +789,10 @@ nodeunitShim({ }, })); - test.done(); - }, - 'grantAttachVolume to instance self'(test: Test) { + }); + + test('grantAttachVolume to instance self', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new Vpc(stack, 'Vpc'); @@ -763,10 +866,10 @@ nodeunitShim({ ], }, ResourcePart.Properties)); - test.done(); - }, - 'grantAttachVolume to instance self with suffix'(test: Test) { + }); + + test('grantAttachVolume to instance self with suffix', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new Vpc(stack, 'Vpc'); @@ -839,10 +942,10 @@ nodeunitShim({ }, ], }, ResourcePart.Properties)); - test.done(); - }, - 'grantDetachVolume to any instance'(test: Test) { + }); + + test('grantDetachVolume to any instance', () => { // GIVEN const stack = new cdk.Stack(); const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); @@ -909,10 +1012,10 @@ nodeunitShim({ }], }, })); - test.done(); - }, - 'grantDetachVolume from specific instance'(test: Test) { + }); + + test('grantDetachVolume from specific instance', () => { // GIVEN const stack = new cdk.Stack(); const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); @@ -997,10 +1100,10 @@ nodeunitShim({ }, })); - test.done(); - }, - 'grantDetachVolume from instance self'(test: Test) { + }); + + test('grantDetachVolume from instance self', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new Vpc(stack, 'Vpc'); @@ -1074,10 +1177,10 @@ nodeunitShim({ ], }, ResourcePart.Properties)); - test.done(); - }, - 'grantDetachVolume from instance self with suffix'(test: Test) { + }); + + test('grantDetachVolume from instance self with suffix', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new Vpc(stack, 'Vpc'); @@ -1150,10 +1253,10 @@ nodeunitShim({ }, ], }, ResourcePart.Properties)); - test.done(); - }, - 'validation fromVolumeAttributes'(test: Test) { + }); + + test('validation fromVolumeAttributes', () => { // GIVEN let idx: number = 0; const stack = new cdk.Stack(); @@ -1163,93 +1266,93 @@ nodeunitShim({ }); // THEN - test.doesNotThrow(() => { + expect(() => { Volume.fromVolumeAttributes(stack, `Volume${idx++}`, { volumeId: volume.volumeId, availabilityZone: volume.availabilityZone, }); - }); - test.doesNotThrow(() => { + }).not.toThrow(); + expect(() => { Volume.fromVolumeAttributes(stack, `Volume${idx++}`, { volumeId: 'vol-0123456789abcdefABCDEF', availabilityZone: 'us-east-1a', }); - }); - test.throws(() => { + }).not.toThrow(); + expect(() => { Volume.fromVolumeAttributes(stack, `Volume${idx++}`, { volumeId: ' vol-0123456789abcdefABCDEF', // leading invalid character(s) availabilityZone: 'us-east-1a', }); - }, '`volumeId` does not match expected pattern. Expected `vol-` (ex: `vol-05abe246af`) or a Token'); - test.throws(() => { + }).toThrow('`volumeId` does not match expected pattern. Expected `vol-` (ex: `vol-05abe246af`) or a Token'); + expect(() => { Volume.fromVolumeAttributes(stack, `Volume${idx++}`, { volumeId: 'vol-0123456789abcdefABCDEF ', // trailing invalid character(s) availabilityZone: 'us-east-1a', }); - }, '`volumeId` does not match expected pattern. Expected `vol-` (ex: `vol-05abe246af`) or a Token'); - test.done(); - }, + }).toThrow('`volumeId` does not match expected pattern. Expected `vol-` (ex: `vol-05abe246af`) or a Token'); + + }); - 'validation required props'(test: Test) { + test('validation required props', () => { // GIVEN const stack = new cdk.Stack(); const key = new kms.Key(stack, 'Key'); let idx: number = 0; // THEN - test.throws(() => { + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', }); - }, 'Must provide at least one of `size` or `snapshotId`'); - test.doesNotThrow(() => { + }).toThrow('Must provide at least one of `size` or `snapshotId`'); + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', size: cdk.Size.gibibytes(8), }); - }); - test.doesNotThrow(() => { + }).not.toThrow(); + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', snapshotId: 'snap-000000000', }); - }); - test.doesNotThrow(() => { + }).not.toThrow(); + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', size: cdk.Size.gibibytes(8), snapshotId: 'snap-000000000', }); - }); + }).not.toThrow(); - test.throws(() => { + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', size: cdk.Size.gibibytes(8), encryptionKey: key, }); - }, '`encrypted` must be true when providing an `encryptionKey`.'); - test.throws(() => { + }).toThrow('`encrypted` must be true when providing an `encryptionKey`.'); + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', size: cdk.Size.gibibytes(8), encrypted: false, encryptionKey: key, }); - }, '`encrypted` must be true when providing an `encryptionKey`.'); - test.doesNotThrow(() => { + }).toThrow('`encrypted` must be true when providing an `encryptionKey`.'); + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', size: cdk.Size.gibibytes(8), encrypted: true, encryptionKey: key, }); - }); + }).not.toThrow(); + - test.done(); - }, + }); - 'validation snapshotId'(test: Test) { + test('validation snapshotId', () => { // GIVEN const stack = new cdk.Stack(); const volume = new Volume(stack, 'ForToken', { @@ -1259,114 +1362,160 @@ nodeunitShim({ let idx: number = 0; // THEN - test.doesNotThrow(() => { + expect(() => { // Should not throw if we provide a Token for the snapshotId new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', snapshotId: volume.volumeId, }); - }); - test.doesNotThrow(() => { + }).not.toThrow(); + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', snapshotId: 'snap-0123456789abcdefABCDEF', }); - }); - test.throws(() => { + }).not.toThrow(); + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', snapshotId: ' snap-1234', // leading extra character(s) }); - }, '`snapshotId` does match expected pattern. Expected `snap-` (ex: `snap-05abe246af`) or Token'); - test.throws(() => { + }).toThrow('`snapshotId` does match expected pattern. Expected `snap-` (ex: `snap-05abe246af`) or Token'); + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', snapshotId: 'snap-1234 ', // trailing extra character(s) }); - }, '`snapshotId` does match expected pattern. Expected `snap-` (ex: `snap-05abe246af`) or Token'); + }).toThrow('`snapshotId` does match expected pattern. Expected `snap-` (ex: `snap-05abe246af`) or Token'); + - test.done(); - }, + }); - 'validation iops'(test: Test) { + test('validation iops', () => { // GIVEN const stack = new cdk.Stack(); let idx: number = 0; // THEN // Test: Type of volume + for (const volumeType of [ + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, + EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, + ]) { + expect(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + iops: 3000, + volumeType, + }); + }).not.toThrow(); + } + + for (const volumeType of [ + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, + ]) { + expect(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + volumeType, + }); + }).toThrow(/`iops` must be specified if the `volumeType` is/); + } + for (const volumeType of [ EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, EbsDeviceVolumeType.THROUGHPUT_OPTIMIZED_HDD, EbsDeviceVolumeType.COLD_HDD, EbsDeviceVolumeType.MAGNETIC, ]) { - test.throws(() => { + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', size: cdk.Size.gibibytes(500), iops: 100, volumeType, }); - }, '`iops` may only be specified if the `volumeType` is `PROVISIONED_IOPS_SSD`/`IO1`'); + }).toThrow(/`iops` may only be specified if the `volumeType` is/); } // Test: iops in range - test.throws(() => { - new Volume(stack, `Volume${idx++}`, { - availabilityZone: 'us-east-1a', - size: cdk.Size.gibibytes(10), - iops: 99, - volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, - }); - }, '`iops` must be in the range 100 to 64,000, inclusive.'); - test.doesNotThrow(() => { - new Volume(stack, `Volume${idx++}`, { - availabilityZone: 'us-east-1a', - size: cdk.Size.gibibytes(10), - iops: 100, - volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, - }); - }); - test.doesNotThrow(() => { - new Volume(stack, `Volume${idx++}`, { - availabilityZone: 'us-east-1a', - size: cdk.Size.gibibytes(1300), - iops: 64000, - volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, - }); - }); - test.throws(() => { - new Volume(stack, `Volume${idx++}`, { - availabilityZone: 'us-east-1a', - size: cdk.Size.gibibytes(1300), - iops: 64001, - volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, - }); - }, '`iops` must be in the range 100 to 64,000, inclusive.'); + for (const testData of [ + [EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, 3000, 16000], + [EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, 100, 64000], + [EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, 100, 64000], + ]) { + const volumeType = testData[0] as EbsDeviceVolumeType; + const min = testData[1] as number; + const max = testData[2] as number; + expect(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.tebibytes(10), + volumeType, + iops: min - 1, + }); + }).toThrow(/iops must be between/); + expect(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.tebibytes(10), + volumeType, + iops: min, + }); + }).not.toThrow(); + expect(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.tebibytes(10), + volumeType, + iops: max, + }); + }).not.toThrow(); + expect(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.tebibytes(10), + volumeType, + iops: max + 1, + }); + }).toThrow(/iops must be between/); + } // Test: iops ratio - test.doesNotThrow(() => { - new Volume(stack, `Volume${idx++}`, { - availabilityZone: 'us-east-1a', - size: cdk.Size.gibibytes(10), - iops: 500, - volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, - }); - }); - test.throws(() => { - new Volume(stack, `Volume${idx++}`, { - availabilityZone: 'us-east-1a', - size: cdk.Size.gibibytes(10), - iops: 501, - volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, - }); - }, '`iops` has a maximum ratio of 50 IOPS/GiB.'); + for (const testData of [ + [EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, 500], + [EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, 50], + [EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, 500], + ]) { + const volumeType = testData[0] as EbsDeviceVolumeType; + const max = testData[1] as number; + const size = 10; + expect(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(size), + volumeType, + iops: max * size, + }); + }).not.toThrow(); + expect(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(size), + volumeType, + iops: max * size + 1, + }); + }).toThrow(/iops has a maximum ratio of/); + } - test.done(); - }, - 'validation multi-attach'(test: Test) { + }); + + test('validation multi-attach', () => { // GIVEN const stack = new cdk.Stack(); let idx: number = 0; @@ -1374,70 +1523,109 @@ nodeunitShim({ // THEN for (const volumeType of [ EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, + EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, EbsDeviceVolumeType.THROUGHPUT_OPTIMIZED_HDD, EbsDeviceVolumeType.COLD_HDD, EbsDeviceVolumeType.MAGNETIC, ]) { - test.throws(() => { - new Volume(stack, `Volume${idx++}`, { - availabilityZone: 'us-east-1a', - size: cdk.Size.gibibytes(500), - enableMultiAttach: true, - volumeType, - }); - }, 'multi-attach is supported exclusively on `PROVISIONED_IOPS_SSD` volumes.'); + if ( + [ + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, + ].includes(volumeType) + ) { + expect(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + enableMultiAttach: true, + volumeType, + iops: 100, + }); + }).not.toThrow(); + } else { + expect(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + enableMultiAttach: true, + volumeType, + }); + }).toThrow(/multi-attach is supported exclusively/); + } } - test.done(); - }, - 'validation size in range'(test: Test) { + }); + + test('validation size in range', () => { // GIVEN const stack = new cdk.Stack(); let idx: number = 0; // THEN for (const testData of [ - [EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, 1, 16000], - [EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, 4, 16000], - [EbsDeviceVolumeType.THROUGHPUT_OPTIMIZED_HDD, 500, 16000], - [EbsDeviceVolumeType.COLD_HDD, 500, 16000], - [EbsDeviceVolumeType.MAGNETIC, 1, 1000], + [EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, 1, 16384], + [EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3, 1, 16384], + [EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, 4, 16384], + [EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, 4, 16384], + [EbsDeviceVolumeType.THROUGHPUT_OPTIMIZED_HDD, 125, 16384], + [EbsDeviceVolumeType.COLD_HDD, 125, 16384], + [EbsDeviceVolumeType.MAGNETIC, 1, 1024], ]) { const volumeType = testData[0] as EbsDeviceVolumeType; const min = testData[1] as number; const max = testData[2] as number; - test.throws(() => { + const iops = [ + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + EbsDeviceVolumeType.PROVISIONED_IOPS_SSD_IO2, + ].includes(volumeType) ? 100 : null; + + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', size: cdk.Size.gibibytes(min - 1), volumeType, + ...iops + ? { iops } + : {}, }); - }, `\`${volumeType}\` volumes must be between ${min} GiB and ${max} GiB in size.`); - test.doesNotThrow(() => { + }).toThrow(/volumes must be between/); + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', size: cdk.Size.gibibytes(min), volumeType, + ...iops + ? { iops } + : {}, }); - }); - test.doesNotThrow(() => { + }).not.toThrow(); + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', size: cdk.Size.gibibytes(max), volumeType, + ...iops + ? { iops } + : {}, }); - }); - test.throws(() => { + }).not.toThrow(); + expect(() => { new Volume(stack, `Volume${idx++}`, { availabilityZone: 'us-east-1a', size: cdk.Size.gibibytes(max + 1), volumeType, + ...iops + ? { iops } + : {}, }); - }, `\`${volumeType}\` volumes must be between ${min} GiB and ${max} GiB in size.`); + }).toThrow(/volumes must be between/); } - test.done(); - }, + + }); }); diff --git a/packages/@aws-cdk/aws-ec2/test/vpc.from-lookup.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc.from-lookup.test.ts index 5555dc3fa9ed7..12ed7d05329d5 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc.from-lookup.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc.from-lookup.test.ts @@ -210,6 +210,47 @@ nodeunitShim({ test.done(); }, + 'subnets in imported VPC has all expected attributes'(test: Test) { + const previous = mockVpcContextProviderWith(test, { + vpcId: 'vpc-1234', + subnetGroups: [ + { + name: 'Public', + type: cxapi.VpcSubnetGroupType.PUBLIC, + subnets: [ + { + subnetId: 'pub-sub-in-us-east-1a', + availabilityZone: 'us-east-1a', + routeTableId: 'rt-123', + cidr: '10.100.0.0/24', + }, + ], + }, + ], + }, options => { + test.deepEqual(options.filter, { + isDefault: 'true', + }); + + test.equal(options.subnetGroupNameTag, undefined); + }); + + const stack = new Stack(); + const vpc = Vpc.fromLookup(stack, 'Vpc', { + isDefault: true, + }); + + let subnet = vpc.publicSubnets[0]; + + test.equal(subnet.availabilityZone, 'us-east-1a'); + test.equal(subnet.subnetId, 'pub-sub-in-us-east-1a'); + test.equal(subnet.routeTable.routeTableId, 'rt-123'); + test.equal(subnet.ipv4CidrBlock, '10.100.0.0/24'); + + + restoreContextProvider(previous); + test.done(); + }, }, }); diff --git a/packages/@aws-cdk/aws-ec2/test/vpn.test.ts b/packages/@aws-cdk/aws-ec2/test/vpn.test.ts index 72be541bf45e2..4cf8880c3b0ab 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpn.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpn.test.ts @@ -1,5 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; -import { Duration, Stack } from '@aws-cdk/core'; +import { Duration, Stack, Token } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; import { PublicSubnet, Vpc, VpnConnection } from '../lib'; @@ -322,4 +322,24 @@ nodeunitShim({ })); test.done(); }, + 'can add a vpn connection with a Token as customer gateway ip'(test:Test) { + // GIVEN + const stack = new Stack(); + const token = Token.asAny('192.0.2.1'); + + // WHEN + new Vpc(stack, 'VpcNetwork', { + vpnConnections: { + VpnConnection: { + ip: token as any, + }, + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::CustomerGateway', { + IpAddress: '192.0.2.1', + })); + test.done(); + }, }); diff --git a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts index 2f6f5ff436baa..26a3a40f35335 100644 --- a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts +++ b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts @@ -2,10 +2,14 @@ import * as fs from 'fs'; import * as path from 'path'; import * as assets from '@aws-cdk/assets'; import * as ecr from '@aws-cdk/aws-ecr'; -import { Annotations, Construct as CoreConstruct, FeatureFlags, IgnoreMode, Stack, Token } from '@aws-cdk/core'; +import { Annotations, FeatureFlags, IgnoreMode, Stack, Token } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + /** * Options for DockerImageAsset */ diff --git a/packages/@aws-cdk/aws-ecr/README.md b/packages/@aws-cdk/aws-ecr/README.md index 6278238b84eab..9bce0c80e132c 100644 --- a/packages/@aws-cdk/aws-ecr/README.md +++ b/packages/@aws-cdk/aws-ecr/README.md @@ -51,11 +51,29 @@ grants an IAM user access to call this API. ```ts import * as iam from '@aws-cdk/aws-iam'; +import * as ecr from '@aws-cdk/aws-ecr'; const user = new iam.User(this, 'User', { ... }); -iam.AuthorizationToken.grantRead(user); +ecr.AuthorizationToken.grantRead(user); ``` +If you access images in the [Public ECR Gallery](https://gallery.ecr.aws/) as well, it is recommended you authenticate to the regsitry to benefit from +higher rate and bandwidth limits. + +> See `Pricing` in https://aws.amazon.com/blogs/aws/amazon-ecr-public-a-new-public-container-registry/ and [Service quotas](https://docs.aws.amazon.com/AmazonECR/latest/public/public-service-quotas.html). + +The following code snippet grants an IAM user access to retrieve an authorization token for the public gallery. + +```ts +import * as iam from '@aws-cdk/aws-iam'; +import * as ecr from '@aws-cdk/aws-ecr'; + +const user = new iam.User(this, 'User', { ... }); +ecr.PublicGalleryAuthorizationToken.grantRead(user); +``` + +This user can then proceed to login to the registry using one of the [authentication methods](https://docs.aws.amazon.com/AmazonECR/latest/public/public-registries.html#public-registry-auth). + ## Automatically clean up repositories You can set life cycle rules to automatically clean up old images from your diff --git a/packages/@aws-cdk/aws-ecr/lib/auth-token.ts b/packages/@aws-cdk/aws-ecr/lib/auth-token.ts index 52c10cc513d0a..63484bbed0199 100644 --- a/packages/@aws-cdk/aws-ecr/lib/auth-token.ts +++ b/packages/@aws-cdk/aws-ecr/lib/auth-token.ts @@ -1,7 +1,9 @@ import * as iam from '@aws-cdk/aws-iam'; /** - * Authorization token to access ECR repositories via Docker CLI. + * Authorization token to access private ECR repositories in the current environment via Docker CLI. + * + * @see https://docs.aws.amazon.com/AmazonECR/latest/userguide/registry_auth.html */ export class AuthorizationToken { /** @@ -18,3 +20,27 @@ export class AuthorizationToken { private constructor() { } } + +/** + * Authorization token to access the global public ECR Gallery via Docker CLI. + * + * @see https://docs.aws.amazon.com/AmazonECR/latest/public/public-registries.html#public-registry-auth + */ +export class PublicGalleryAuthorizationToken { + + /** + * Grant access to retrieve an authorization token. + */ + public static grantRead(grantee: iam.IGrantable) { + grantee.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['ecr-public:GetAuthorizationToken', 'sts:GetServiceBearerToken'], + // GetAuthorizationToken only allows '*'. See https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonelasticcontainerregistry.html#amazonelasticcontainerregistry-actions-as-permissions + // GetServiceBearerToken only allows '*'. See https://docs.aws.amazon.com/service-authorization/latest/reference/list_awssecuritytokenservice.html#awssecuritytokenservice-actions-as-permissions + resources: ['*'], + })); + } + + private constructor() { + } + +} diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 36e14cf861adc..83c05ba1ed308 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -202,7 +202,7 @@ export abstract class RepositoryBase extends Resource implements IRepository { detail: { 'repository-name': [this.repositoryName], 'scan-status': ['COMPLETE'], - 'image-tags': options.imageTags ? options.imageTags : undefined, + 'image-tags': options.imageTags ?? undefined, }, }); return rule; @@ -526,7 +526,7 @@ export class Repository extends RepositoryBase { for (const rule of prioritizedRules.concat(autoPrioritizedRules).concat(anyRules)) { ret.push({ ...rule, - rulePriority: rule.rulePriority !== undefined ? rule.rulePriority : autoPrio++, + rulePriority: rule.rulePriority ?? autoPrio++, }); } @@ -557,7 +557,7 @@ function renderLifecycleRule(rule: LifecycleRule) { tagStatus: rule.tagStatus || TagStatus.ANY, tagPrefixList: rule.tagPrefixList, countType: rule.maxImageAge !== undefined ? CountType.SINCE_IMAGE_PUSHED : CountType.IMAGE_COUNT_MORE_THAN, - countNumber: rule.maxImageAge !== undefined ? rule.maxImageAge.toDays() : rule.maxImageCount, + countNumber: rule.maxImageAge?.toDays() ?? rule.maxImageCount, countUnit: rule.maxImageAge !== undefined ? 'days' : undefined, }, action: { diff --git a/packages/@aws-cdk/aws-ecr/test/test.auth-token.ts b/packages/@aws-cdk/aws-ecr/test/test.auth-token.ts index 4e9e12e4fb078..bb1e13c5566b4 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.auth-token.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.auth-token.ts @@ -2,10 +2,10 @@ import { expect, haveResourceLike } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import { Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { AuthorizationToken } from '../lib'; +import { AuthorizationToken, PublicGalleryAuthorizationToken } from '../lib'; export = { - 'grant()'(test: Test) { + 'AuthorizationToken.grantRead()'(test: Test) { // GIVEN const stack = new Stack(); const user = new iam.User(stack, 'User'); @@ -28,4 +28,32 @@ export = { test.done(); }, + + 'PublicGalleryAuthorizationToken.grantRead()'(test: Test) { + // GIVEN + const stack = new Stack(); + const user = new iam.User(stack, 'User'); + + // WHEN + PublicGalleryAuthorizationToken.grantRead(user); + + // THEN + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'ecr-public:GetAuthorizationToken', + 'sts:GetServiceBearerToken', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + }, + })); + + test.done(); + }, + }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index 7a80d93aad346..38915031f4346 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -427,3 +427,17 @@ const loadBalancedFargateService = new ApplicationLoadBalancedFargateService(sta }, }); ``` + +### Set PlatformVersion for ScheduledFargateTask + +```ts +const scheduledFargateTask = new ScheduledFargateTask(stack, 'ScheduledFargateTask', { + cluster, + scheduledFargateTaskImageOptions: { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }, + schedule: events.Schedule.expression('rate(1 minute)'), + platformVersion: ecs.FargatePlatformVersion.VERSION1_4, +}); +``` 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 769756999ad0c..74411bb217558 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 @@ -370,21 +370,19 @@ export abstract class ApplicationLoadBalancedServiceBase extends CoreConstruct { } this.desiredCount = props.desiredCount || 1; - const internetFacing = props.publicLoadBalancer !== undefined ? props.publicLoadBalancer : true; + const internetFacing = props.publicLoadBalancer ?? true; const lbProps = { vpc: this.cluster.vpc, internetFacing, }; - const loadBalancer = props.loadBalancer !== undefined ? props.loadBalancer - : new ApplicationLoadBalancer(this, 'LB', lbProps); + const loadBalancer = props.loadBalancer ?? new ApplicationLoadBalancer(this, 'LB', lbProps); if (props.certificate !== undefined && props.protocol !== undefined && props.protocol !== ApplicationProtocol.HTTPS) { throw new Error('The HTTPS protocol must be used when a certificate is given'); } - const protocol = props.protocol !== undefined ? props.protocol : - (props.certificate ? ApplicationProtocol.HTTPS : ApplicationProtocol.HTTP); + const protocol = props.protocol ?? (props.certificate ? ApplicationProtocol.HTTPS : ApplicationProtocol.HTTP); if (protocol !== ApplicationProtocol.HTTPS && props.redirectHTTP === true) { throw new Error('The HTTPS protocol must be used when redirecting HTTP traffic'); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts index 67ce9a02e77b6..f3eb132e934ae 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts @@ -478,10 +478,8 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreCon * Create log driver if logging is enabled. */ private createLogDriver(enableLoggingProp?: boolean, logDriverProp?: LogDriver): LogDriver | undefined { - const enableLogging = enableLoggingProp !== undefined ? enableLoggingProp : true; - const logDriver = logDriverProp !== undefined - ? logDriverProp : enableLogging - ? this.createAWSLogDriver(this.node.id) : undefined; + const enableLogging = enableLoggingProp ?? true; + const logDriver = logDriverProp ?? (enableLogging ? this.createAWSLogDriver(this.node.id) : undefined); return logDriver; } @@ -522,7 +520,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreCon } private createLoadBalancer(name: string, publicLoadBalancer?: boolean): ApplicationLoadBalancer { - const internetFacing = publicLoadBalancer !== undefined ? publicLoadBalancer : true; + const internetFacing = publicLoadBalancer ?? true; const lbProps = { vpc: this.cluster.vpc, internetFacing, @@ -532,7 +530,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreCon } private createListenerProtocol(listenerProtocol?: ApplicationProtocol, certificate?: ICertificate): ApplicationProtocol { - return listenerProtocol !== undefined ? listenerProtocol : (certificate ? ApplicationProtocol.HTTPS : ApplicationProtocol.HTTP); + return listenerProtocol ?? (certificate ? ApplicationProtocol.HTTPS : ApplicationProtocol.HTTP); } private createListenerCertificate(listenerName: string, certificate?: ICertificate, domainName?: string, domainZone?: IHostedZone): ICertificate { 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 ffdcb3b75912a..656cc19d19d43 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 @@ -308,18 +308,15 @@ export abstract class NetworkLoadBalancedServiceBase extends CoreConstruct { } this.desiredCount = props.desiredCount || 1; - const internetFacing = props.publicLoadBalancer !== undefined ? props.publicLoadBalancer : true; + const internetFacing = props.publicLoadBalancer ?? true; const lbProps = { vpc: this.cluster.vpc, internetFacing, }; - const loadBalancer = props.loadBalancer !== undefined ? props.loadBalancer : - new NetworkLoadBalancer(this, 'LB', lbProps); - - const listenerPort = props.listenerPort !== undefined ? props.listenerPort : 80; - + const loadBalancer = props.loadBalancer ?? new NetworkLoadBalancer(this, 'LB', lbProps); + const listenerPort = props.listenerPort ?? 80; const targetProps = { port: 80, }; diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts index d3f74588fd341..d34a6b548076d 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts @@ -384,10 +384,8 @@ export abstract class NetworkMultipleTargetGroupsServiceBase extends CoreConstru * Create log driver if logging is enabled. */ private createLogDriver(enableLoggingProp?: boolean, logDriverProp?: LogDriver): LogDriver | undefined { - const enableLogging = enableLoggingProp !== undefined ? enableLoggingProp : true; - const logDriver = logDriverProp !== undefined - ? logDriverProp : enableLogging - ? this.createAWSLogDriver(this.node.id) : undefined; + const enableLogging = enableLoggingProp ?? true; + const logDriver = logDriverProp ?? (enableLogging ? this.createAWSLogDriver(this.node.id) : undefined); return logDriver; } @@ -413,7 +411,7 @@ export abstract class NetworkMultipleTargetGroupsServiceBase extends CoreConstru } private createLoadBalancer(name: string, publicLoadBalancer?: boolean): NetworkLoadBalancer { - const internetFacing = publicLoadBalancer !== undefined ? publicLoadBalancer : true; + const internetFacing = publicLoadBalancer ?? true; const lbProps = { vpc: this.cluster.vpc, internetFacing, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts index 66bdea9619629..3248514931f4d 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts @@ -262,22 +262,18 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct { // Setup autoscaling scaling intervals const defaultScalingSteps = [{ upper: 0, change: -1 }, { lower: 100, change: +1 }, { lower: 500, change: +5 }]; - this.scalingSteps = props.scalingSteps !== undefined ? props.scalingSteps : defaultScalingSteps; + this.scalingSteps = props.scalingSteps ?? defaultScalingSteps; // Create log driver if logging is enabled - const enableLogging = props.enableLogging !== undefined ? props.enableLogging : true; - this.logDriver = props.logDriver !== undefined - ? props.logDriver - : enableLogging - ? this.createAWSLogDriver(this.node.id) - : undefined; + const enableLogging = props.enableLogging ?? true; + this.logDriver = props.logDriver ?? (enableLogging ? this.createAWSLogDriver(this.node.id) : undefined); // Add the queue name to environment variables this.environment = { ...(props.environment || {}), QUEUE_NAME: this.sqsQueue.queueName }; this.secrets = props.secrets; // Determine the desired task count (minimum) and maximum scaling capacity - this.desiredCount = props.desiredTaskCount !== undefined ? props.desiredTaskCount : 1; + this.desiredCount = props.desiredTaskCount ?? 1; this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount); if (!this.desiredCount && !this.maxCapacity) { diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts index 259e375b1973c..38ac4e30a79af 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/scheduled-task-base.ts @@ -165,11 +165,20 @@ export abstract class ScheduledTaskBase extends CoreConstruct { subnetSelection: this.subnetSelection, }); - this.eventRule.addTarget(eventRuleTarget); + this.addTaskAsTarget(eventRuleTarget); return eventRuleTarget; } + /** + * Adds task as a target of the scheduled event rule. + * + * @param ecsTaskTarget the EcsTask to add to the event rule + */ + protected addTaskAsTarget(ecsTaskTarget: EcsTask) { + this.eventRule.addTarget(ecsTaskTarget); + } + /** * Returns the default cluster. */ diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts index 9cec9e1d9e083..b9bdcf2d100ad 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts @@ -97,12 +97,10 @@ export class ApplicationLoadBalancedEc2Service extends ApplicationLoadBalancedSe }); // Create log driver if logging is enabled - const enableLogging = taskImageOptions.enableLogging !== undefined ? taskImageOptions.enableLogging : true; - const logDriver = taskImageOptions.logDriver !== undefined - ? taskImageOptions.logDriver : enableLogging - ? this.createAWSLogDriver(this.node.id) : undefined; + const enableLogging = taskImageOptions.enableLogging ?? true; + const logDriver = taskImageOptions.logDriver ?? (enableLogging ? this.createAWSLogDriver(this.node.id) : undefined); - const containerName = taskImageOptions.containerName !== undefined ? taskImageOptions.containerName : 'web'; + const containerName = taskImageOptions.containerName ?? 'web'; const container = this.taskDefinition.addContainer(containerName, { image: taskImageOptions.image, cpu: props.cpu, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts index 4e6fa5c04b6f3..6ed6b6b71802f 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts @@ -94,7 +94,7 @@ export class ApplicationMultipleTargetGroupsEc2Service extends ApplicationMultip taskRole: taskImageOptions.taskRole, }); - const containerName = taskImageOptions.containerName !== undefined ? taskImageOptions.containerName : 'web'; + const containerName = taskImageOptions.containerName ?? 'web'; const container = this.taskDefinition.addContainer(containerName, { image: taskImageOptions.image, cpu: props.cpu, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts index 2d2c00fcf345e..fae46b68e7380 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts @@ -95,12 +95,10 @@ export class NetworkLoadBalancedEc2Service extends NetworkLoadBalancedServiceBas }); // Create log driver if logging is enabled - const enableLogging = taskImageOptions.enableLogging !== undefined ? taskImageOptions.enableLogging : true; - const logDriver = taskImageOptions.logDriver !== undefined - ? taskImageOptions.logDriver : enableLogging - ? this.createAWSLogDriver(this.node.id) : undefined; + const enableLogging = taskImageOptions.enableLogging ?? true; + const logDriver = taskImageOptions.logDriver ?? (enableLogging ? this.createAWSLogDriver(this.node.id) : undefined); - const containerName = taskImageOptions.containerName !== undefined ? taskImageOptions.containerName : 'web'; + const containerName = taskImageOptions.containerName ?? 'web'; const container = this.taskDefinition.addContainer(containerName, { image: taskImageOptions.image, cpu: props.cpu, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts index a283c9ab55dec..eb8392d3b2148 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts @@ -93,7 +93,7 @@ export class NetworkMultipleTargetGroupsEc2Service extends NetworkMultipleTarget taskRole: taskImageOptions.taskRole, }); - const containerName = taskImageOptions.containerName !== undefined ? taskImageOptions.containerName : 'web'; + const containerName = taskImageOptions.containerName ?? 'web'; const container = this.taskDefinition.addContainer(containerName, { image: taskImageOptions.image, cpu: props.cpu, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts index 7350ae82bb2cc..7f64a18df9ff8 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/scheduled-ecs-task.ts @@ -107,7 +107,7 @@ export class ScheduledEc2Task extends ScheduledTaskBase { command: taskImageOptions.command, environment: taskImageOptions.environment, secrets: taskImageOptions.secrets, - logging: taskImageOptions.logDriver !== undefined ? taskImageOptions.logDriver : this.createAWSLogDriver(this.node.id), + logging: taskImageOptions.logDriver ?? this.createAWSLogDriver(this.node.id), }); } else { throw new Error('You must specify a taskDefinition or image'); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts index 00049e662ee0f..2ae468bcae558 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts @@ -117,7 +117,7 @@ export class ApplicationLoadBalancedFargateService extends ApplicationLoadBalanc constructor(scope: Construct, id: string, props: ApplicationLoadBalancedFargateServiceProps = {}) { super(scope, id, props); - this.assignPublicIp = props.assignPublicIp !== undefined ? props.assignPublicIp : false; + this.assignPublicIp = props.assignPublicIp ?? false; if (props.taskDefinition && props.taskImageOptions) { throw new Error('You must specify either a taskDefinition or an image, not both.'); @@ -134,12 +134,12 @@ export class ApplicationLoadBalancedFargateService extends ApplicationLoadBalanc }); // Create log driver if logging is enabled - const enableLogging = taskImageOptions.enableLogging !== undefined ? taskImageOptions.enableLogging : true; + const enableLogging = taskImageOptions.enableLogging ?? true; const logDriver = taskImageOptions.logDriver !== undefined ? taskImageOptions.logDriver : enableLogging ? this.createAWSLogDriver(this.node.id) : undefined; - const containerName = taskImageOptions.containerName !== undefined ? taskImageOptions.containerName : 'web'; + const containerName = taskImageOptions.containerName ?? 'web'; const container = this.taskDefinition.addContainer(containerName, { image: taskImageOptions.image, logging: logDriver, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts index a5840bc2f2e76..495049dfccfa8 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts @@ -113,7 +113,7 @@ export class ApplicationMultipleTargetGroupsFargateService extends ApplicationMu constructor(scope: Construct, id: string, props: ApplicationMultipleTargetGroupsFargateServiceProps = {}) { super(scope, id, props); - this.assignPublicIp = props.assignPublicIp !== undefined ? props.assignPublicIp : false; + this.assignPublicIp = props.assignPublicIp ?? false; if (props.taskDefinition && props.taskImageOptions) { throw new Error('You must specify only one of TaskDefinition or TaskImageOptions.'); @@ -129,7 +129,7 @@ export class ApplicationMultipleTargetGroupsFargateService extends ApplicationMu family: taskImageOptions.family, }); - const containerName = taskImageOptions.containerName !== undefined ? taskImageOptions.containerName : 'web'; + const containerName = taskImageOptions.containerName ?? 'web'; const container = this.taskDefinition.addContainer(containerName, { image: taskImageOptions.image, logging: this.logDriver, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts index 10dcfebdc2f80..4aad4b31e7efe 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts @@ -106,7 +106,7 @@ export class NetworkLoadBalancedFargateService extends NetworkLoadBalancedServic constructor(scope: Construct, id: string, props: NetworkLoadBalancedFargateServiceProps = {}) { super(scope, id, props); - this.assignPublicIp = props.assignPublicIp !== undefined ? props.assignPublicIp : false; + this.assignPublicIp = props.assignPublicIp ?? false; if (props.taskDefinition && props.taskImageOptions) { throw new Error('You must specify either a taskDefinition or an image, not both.'); @@ -123,12 +123,10 @@ export class NetworkLoadBalancedFargateService extends NetworkLoadBalancedServic }); // Create log driver if logging is enabled - const enableLogging = taskImageOptions.enableLogging !== undefined ? taskImageOptions.enableLogging : true; - const logDriver = taskImageOptions.logDriver !== undefined - ? taskImageOptions.logDriver : enableLogging - ? this.createAWSLogDriver(this.node.id) : undefined; + const enableLogging = taskImageOptions.enableLogging ?? true; + const logDriver = taskImageOptions.logDriver ?? (enableLogging ? this.createAWSLogDriver(this.node.id) : undefined); - const containerName = taskImageOptions.containerName !== undefined ? taskImageOptions.containerName : 'web'; + const containerName = taskImageOptions.containerName ?? 'web'; const container = this.taskDefinition.addContainer(containerName, { image: taskImageOptions.image, logging: logDriver, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts index 0d97d730daeab..dab033b1938ce 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts @@ -113,7 +113,7 @@ export class NetworkMultipleTargetGroupsFargateService extends NetworkMultipleTa constructor(scope: Construct, id: string, props: NetworkMultipleTargetGroupsFargateServiceProps = {}) { super(scope, id, props); - this.assignPublicIp = props.assignPublicIp !== undefined ? props.assignPublicIp : false; + this.assignPublicIp = props.assignPublicIp ?? false; if (props.taskDefinition && props.taskImageOptions) { throw new Error('You must specify only one of TaskDefinition or TaskImageOptions.'); @@ -129,7 +129,7 @@ export class NetworkMultipleTargetGroupsFargateService extends NetworkMultipleTa family: taskImageOptions.family, }); - const containerName = taskImageOptions.containerName !== undefined ? taskImageOptions.containerName : 'web'; + const containerName = taskImageOptions.containerName ?? 'web'; const container = this.taskDefinition.addContainer(containerName, { image: taskImageOptions.image, logging: this.logDriver, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts index 8ad898693b6bd..ab46883fa9c90 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/scheduled-fargate-task.ts @@ -1,4 +1,5 @@ -import { FargateTaskDefinition } from '@aws-cdk/aws-ecs'; +import { FargateTaskDefinition, FargatePlatformVersion } from '@aws-cdk/aws-ecs'; +import { EcsTask } from '@aws-cdk/aws-events-targets'; import { Construct } from 'constructs'; import { ScheduledTaskBase, ScheduledTaskBaseProps, ScheduledTaskImageProps } from '../base/scheduled-task-base'; @@ -21,6 +22,17 @@ export interface ScheduledFargateTaskProps extends ScheduledTaskBaseProps { * @default none */ readonly scheduledFargateTaskImageOptions?: ScheduledFargateTaskImageOptions; + + /** + * The platform version on which to run your service. + * + * If one is not specified, the LATEST platform version is used by default. For more information, see + * [AWS Fargate Platform Versions](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html) + * in the Amazon Elastic Container Service Developer Guide. + * + * @default Latest + */ + readonly platformVersion?: FargatePlatformVersion; } /** @@ -103,12 +115,21 @@ export class ScheduledFargateTask extends ScheduledTaskBase { command: taskImageOptions.command, environment: taskImageOptions.environment, secrets: taskImageOptions.secrets, - logging: taskImageOptions.logDriver !== undefined ? taskImageOptions.logDriver : this.createAWSLogDriver(this.node.id), + logging: taskImageOptions.logDriver ?? this.createAWSLogDriver(this.node.id), }); } else { throw new Error('You must specify one of: taskDefinition or image'); } - this.addTaskDefinitionToEventTarget(this.taskDefinition); + // Use the EcsTask as the target of the EventRule + const eventRuleTarget = new EcsTask( { + cluster: this.cluster, + taskDefinition: this.taskDefinition, + taskCount: this.desiredTaskCount, + subnetSelection: this.subnetSelection, + platformVersion: props.platformVersion, + }); + + this.addTaskAsTarget(eventRuleTarget); } } diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts index 27367249ee3cb..fa62f079862f1 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.scheduled-fargate-task.ts @@ -294,6 +294,60 @@ export = { ], })); + test.done(); + }, + 'Scheduled Fargate Task - with platformVersion defined'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + new ScheduledFargateTask(stack, 'ScheduledFargateTask', { + cluster, + scheduledFargateTaskImageOptions: { + image: ecs.ContainerImage.fromRegistry('henk'), + memoryLimitMiB: 512, + }, + schedule: events.Schedule.expression('rate(1 minute)'), + platformVersion: ecs.FargatePlatformVersion.VERSION1_4, + }); + + // THEN + expect(stack).to(haveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, + EcsParameters: { + LaunchType: 'FARGATE', + NetworkConfiguration: { + AwsVpcConfiguration: { + AssignPublicIp: 'DISABLED', + SecurityGroups: [ + { + 'Fn::GetAtt': [ + 'ScheduledFargateTaskScheduledTaskDefSecurityGroupE075BC19', + 'GroupId', + ], + }, + ], + Subnets: [ + { + Ref: 'VpcPrivateSubnet1Subnet536B997A', + }, + ], + }, + }, + PlatformVersion: '1.4.0', + TaskCount: 1, + TaskDefinitionArn: { Ref: 'ScheduledFargateTaskScheduledTaskDef521FA675' }, + }, + Id: 'Target0', + Input: '{}', + RoleArn: { 'Fn::GetAtt': ['ScheduledFargateTaskScheduledTaskDefEventsRole6CE19522', 'Arn'] }, + }, + ], + })); + test.done(); }, }; diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 6704ab8d79ca9..5c5160a849835 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -234,8 +234,7 @@ class ApplicationListenerConfig extends ListenerConfig { public addTargets(id: string, target: LoadBalancerTargetOptions, service: BaseService) { const props = this.props || {}; const protocol = props.protocol; - const port = props.port !== undefined ? props.port : (protocol === undefined ? 80 : - (protocol === elbv2.ApplicationProtocol.HTTPS ? 443 : 80)); + const port = props.port ?? (protocol === elbv2.ApplicationProtocol.HTTPS ? 443 : 80); this.listener.addTargets(id, { ... props, targets: [ @@ -260,7 +259,7 @@ class NetworkListenerConfig extends ListenerConfig { * Create and attach a target group to listener. */ public addTargets(id: string, target: LoadBalancerTargetOptions, service: BaseService) { - const port = this.props !== undefined ? this.props.port : 80; + const port = this.props?.port ?? 80; this.listener.addTargets(id, { ... this.props, targets: [ @@ -370,7 +369,7 @@ export abstract class BaseService extends Resource } : undefined, }, propagateTags: props.propagateTags === PropagatedTagSource.NONE ? undefined : props.propagateTags, - enableEcsManagedTags: props.enableECSManagedTags === undefined ? false : props.enableECSManagedTags, + enableEcsManagedTags: props.enableECSManagedTags ?? false, deploymentController: props.deploymentController, launchType: props.deploymentController?.type === DeploymentControllerType.EXTERNAL ? undefined : props.launchType, healthCheckGracePeriodSeconds: this.evaluateHealthGracePeriod(props.healthCheckGracePeriod), @@ -525,7 +524,7 @@ export abstract class BaseService extends Resource * @returns The created CloudMap service */ public enableCloudMap(options: CloudMapOptions): cloudmap.Service { - const sdNamespace = options.cloudMapNamespace !== undefined ? options.cloudMapNamespace : this.cluster.defaultCloudMapNamespace; + const sdNamespace = options.cloudMapNamespace ?? this.cluster.defaultCloudMapNamespace; if (sdNamespace === undefined) { throw new Error('Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster.'); } @@ -739,9 +738,7 @@ export abstract class BaseService extends Resource */ private evaluateHealthGracePeriod(providedHealthCheckGracePeriod?: Duration): IResolvable { return Lazy.any({ - produce: () => providedHealthCheckGracePeriod !== undefined ? providedHealthCheckGracePeriod.toSeconds() : - this.loadBalancers.length > 0 ? 60 : - undefined, + produce: () => providedHealthCheckGracePeriod?.toSeconds() ?? (this.loadBalancers.length > 0 ? 60 : undefined), }); } } 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 ff27f00cb79a8..f7b0f05c92800 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -284,8 +284,7 @@ export class TaskDefinition extends TaskDefinitionBase { props.volumes.forEach(v => this.addVolume(v)); } - this.networkMode = props.networkMode !== undefined ? props.networkMode : - this.isFargateCompatible ? NetworkMode.AWS_VPC : NetworkMode.BRIDGE; + this.networkMode = props.networkMode ?? (this.isFargateCompatible ? NetworkMode.AWS_VPC : NetworkMode.BRIDGE); if (this.isFargateCompatible && this.networkMode !== NetworkMode.AWS_VPC) { throw new Error(`Fargate tasks can only have AwsVpc network mode, got: ${this.networkMode}`); } diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index fcc28784229e5..ca84679e7b970 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -790,7 +790,7 @@ class ImportedCluster extends Resource implements ICluster { this.hasEc2Capacity = props.hasEc2Capacity !== false; this._defaultCloudMapNamespace = props.defaultCloudMapNamespace; - this.clusterArn = props.clusterArn !== undefined ? props.clusterArn : Stack.of(this).formatArn({ + this.clusterArn = props.clusterArn ?? Stack.of(this).formatArn({ service: 'ecs', resource: 'cluster', resourceName: props.clusterName, diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index b4d31c56133fc..983489743482f 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -399,7 +399,7 @@ export class ContainerDefinition extends CoreConstruct { throw new Error('MemoryLimitMiB should not be less than MemoryReservationMiB.'); } } - this.essential = props.essential !== undefined ? props.essential : true; + this.essential = props.essential ?? true; this.taskDefinition = props.taskDefinition; this.memoryLimitSpecified = props.memoryLimitMiB !== undefined || props.memoryReservationMiB !== undefined; this.linuxParameters = props.linuxParameters; @@ -705,10 +705,10 @@ function renderEnvironmentFiles(environmentFiles: EnvironmentFileConfig[]): any[ function renderHealthCheck(hc: HealthCheck): CfnTaskDefinition.HealthCheckProperty { return { command: getHealthCheckCommand(hc), - interval: hc.interval != null ? hc.interval.toSeconds() : 30, - retries: hc.retries !== undefined ? hc.retries : 3, - startPeriod: hc.startPeriod && hc.startPeriod.toSeconds(), - timeout: hc.timeout !== undefined ? hc.timeout.toSeconds() : 5, + interval: hc.interval?.toSeconds() ?? 30, + retries: hc.retries ?? 3, + startPeriod: hc.startPeriod?.toSeconds(), + timeout: hc.timeout?.toSeconds() ?? 5, }; } diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index a119e445e6571..18c6df350fb4e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -181,8 +181,7 @@ export class Ec2Service extends BaseService implements IEc2Service { throw new Error('Only one of SecurityGroup or SecurityGroups can be populated.'); } - const propagateTagsFromSource = props.propagateTaskTagsFrom !== undefined ? props.propagateTaskTagsFrom - : (props.propagateTags !== undefined ? props.propagateTags : PropagatedTagSource.NONE); + const propagateTagsFromSource = props.propagateTaskTagsFrom ?? props.propagateTags ?? PropagatedTagSource.NONE; super(scope, id, { ...props, diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index f70ca796ed3f6..01a9f75665c57 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -147,8 +147,7 @@ export class FargateService extends BaseService implements IFargateService { throw new Error(`The task definition of this service uses at least one container that references a secret JSON field. This feature requires platform version ${FargatePlatformVersion.VERSION1_4} or later.`); } - const propagateTagsFromSource = props.propagateTaskTagsFrom !== undefined ? props.propagateTaskTagsFrom - : (props.propagateTags !== undefined ? props.propagateTags : PropagatedTagSource.NONE); + const propagateTagsFromSource = props.propagateTaskTagsFrom ?? props.propagateTags ?? PropagatedTagSource.NONE; super(scope, id, { ...props, diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 670030e3747bf..86da4b1cfde8e 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -111,7 +111,6 @@ }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { - "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-applicationautoscaling": "0.0.0", "@aws-cdk/aws-autoscaling": "0.0.0", "@aws-cdk/aws-autoscaling-hooktargets": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts index 8581738c6da51..9264c47e4550c 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-task-definition.ts @@ -543,7 +543,7 @@ export = { }); // THEN - expect(stack).notTo(haveResource('AWS::ECR::Repository', {})); + expect(stack).to(haveResource('AWS::ECR::Repository', {})); test.done(); }, diff --git a/packages/@aws-cdk/aws-ecs/test/test.aws-log-driver.ts b/packages/@aws-cdk/aws-ecs/test/test.aws-log-driver.ts index 1ea5942386ad0..a4d5c9af9c53d 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.aws-log-driver.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.aws-log-driver.ts @@ -126,7 +126,7 @@ export = { test.done(); }, - 'without a defined log group'(test: Test) { + 'without a defined log group: creates one anyway'(test: Test) { // GIVEN td.addContainer('Container', { image, @@ -136,7 +136,7 @@ export = { }); // THEN - expect(stack).notTo(haveResource('AWS::Logs::LogGroup', {})); + expect(stack).to(haveResource('AWS::Logs::LogGroup', {})); test.done(); }, diff --git a/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts b/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts index a93ea1b76d5a4..60af6fde51752 100644 --- a/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts +++ b/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts @@ -272,7 +272,7 @@ export class FileSystem extends Resource implements IFileSystem { defaultPort: ec2.Port.tcp(FileSystem.DEFAULT_PORT), }); - const subnets = props.vpc.selectSubnets(props.vpcSubnets); + const subnets = props.vpc.selectSubnets(props.vpcSubnets ?? { onePerAz: true }); // We now have to create the mount target for each of the mentioned subnet let mountTargetCount = 0; diff --git a/packages/@aws-cdk/aws-efs/test/efs-file-system.test.ts b/packages/@aws-cdk/aws-efs/test/efs-file-system.test.ts index 5b9f3c6539a45..d3868a841e0a5 100644 --- a/packages/@aws-cdk/aws-efs/test/efs-file-system.test.ts +++ b/packages/@aws-cdk/aws-efs/test/efs-file-system.test.ts @@ -1,4 +1,4 @@ -import { expect as expectCDK, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { expect as expectCDK, haveResource, ResourcePart, countResources } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; import { RemovalPolicy, Size, Stack, Tags } from '@aws-cdk/core'; @@ -240,3 +240,17 @@ test('can specify backup policy', () => { }, })); }); + +test('can create when using a VPC with multiple subnets per availability zone', () => { + // create a vpc with two subnets in the same availability zone. + const oneAzVpc = new ec2.Vpc(stack, 'Vpc', { + maxAzs: 1, + subnetConfiguration: [{ name: 'One', subnetType: ec2.SubnetType.ISOLATED }, { name: 'Two', subnetType: ec2.SubnetType.ISOLATED }], + natGateways: 0, + }); + new FileSystem(stack, 'EfsFileSystem', { + vpc: oneAzVpc, + }); + // make sure only one mount target is created. + expectCDK(stack).to(countResources('AWS::EFS::MountTarget', 1)); +}); diff --git a/packages/@aws-cdk/aws-eks-legacy/lib/aws-auth.ts b/packages/@aws-cdk/aws-eks-legacy/lib/aws-auth.ts index 276937847d558..933ccbb144ea1 100644 --- a/packages/@aws-cdk/aws-eks-legacy/lib/aws-auth.ts +++ b/packages/@aws-cdk/aws-eks-legacy/lib/aws-auth.ts @@ -1,9 +1,13 @@ import * as iam from '@aws-cdk/aws-iam'; -import { Construct, Lazy, Stack } from '@aws-cdk/core'; +import { Lazy, Stack } from '@aws-cdk/core'; import { Mapping } from './aws-auth-mapping'; import { Cluster } from './cluster'; import { KubernetesResource } from './k8s-resource'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + export interface AwsAuthProps { /** * The EKS cluster to apply this configuration to. diff --git a/packages/@aws-cdk/aws-eks-legacy/lib/cluster-resource.ts b/packages/@aws-cdk/aws-eks-legacy/lib/cluster-resource.ts index 8c7600d3a1d34..81c04ef56653a 100644 --- a/packages/@aws-cdk/aws-eks-legacy/lib/cluster-resource.ts +++ b/packages/@aws-cdk/aws-eks-legacy/lib/cluster-resource.ts @@ -2,10 +2,14 @@ import * as path from 'path'; import * as cfn from '@aws-cdk/aws-cloudformation'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Construct, Duration, Token } from '@aws-cdk/core'; +import { Duration, Token } from '@aws-cdk/core'; import { CfnClusterProps } from './eks.generated'; import { KubectlLayer } from './kubectl-layer'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * A low-level CFN resource Amazon EKS cluster implemented through a custom * resource. diff --git a/packages/@aws-cdk/aws-eks-legacy/lib/cluster.ts b/packages/@aws-cdk/aws-eks-legacy/lib/cluster.ts index 61f3ccc01fe3d..1f8c699180aad 100644 --- a/packages/@aws-cdk/aws-eks-legacy/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks-legacy/lib/cluster.ts @@ -4,7 +4,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import * as ssm from '@aws-cdk/aws-ssm'; -import { Annotations, CfnOutput, Construct, Duration, IResource, Resource, Stack, Token, Tags } from '@aws-cdk/core'; +import { Annotations, CfnOutput, Duration, IResource, Resource, Stack, Token, Tags } from '@aws-cdk/core'; import { AwsAuth } from './aws-auth'; import { ClusterResource } from './cluster-resource'; import { CfnCluster, CfnClusterProps } from './eks.generated'; @@ -14,6 +14,10 @@ import { KubectlLayer } from './kubectl-layer'; import { spotInterruptHandler } from './spot-interrupt-handler'; import { renderUserData } from './user-data'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + // defaults are based on https://eksctl.io const DEFAULT_CAPACITY_COUNT = 2; const DEFAULT_CAPACITY_TYPE = ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE); @@ -378,7 +382,7 @@ export class Cluster extends Resource implements ICluster { }; let resource; - this.kubectlEnabled = props.kubectlEnabled === undefined ? true : props.kubectlEnabled; + this.kubectlEnabled = props.kubectlEnabled ?? true; if (this.kubectlEnabled) { resource = new ClusterResource(this, 'Resource', clusterProps); this._defaultMastersRole = resource.creationRole; @@ -425,13 +429,13 @@ export class Cluster extends Resource implements ICluster { } // allocate default capacity if non-zero (or default). - const desiredCapacity = props.defaultCapacity === undefined ? DEFAULT_CAPACITY_COUNT : props.defaultCapacity; + const desiredCapacity = props.defaultCapacity ?? DEFAULT_CAPACITY_COUNT; if (desiredCapacity > 0) { const instanceType = props.defaultCapacityInstance || DEFAULT_CAPACITY_TYPE; this.defaultCapacity = this.addCapacity('DefaultCapacity', { instanceType, desiredCapacity }); } - const outputConfigCommand = props.outputConfigCommand === undefined ? true : props.outputConfigCommand; + const outputConfigCommand = props.outputConfigCommand ?? true; if (outputConfigCommand) { const postfix = commonCommandOptions.join(' '); new CfnOutput(this, 'ConfigCommand', { value: `${updateConfigCommandPrefix} ${postfix}` }); @@ -508,7 +512,7 @@ export class Cluster extends Resource implements ICluster { autoScalingGroup.connections.allowToAnyIpv4(ec2.Port.allUdp()); autoScalingGroup.connections.allowToAnyIpv4(ec2.Port.allIcmp()); - const bootstrapEnabled = options.bootstrapEnabled !== undefined ? options.bootstrapEnabled : true; + const bootstrapEnabled = options.bootstrapEnabled ?? true; if (options.bootstrapOptions && !bootstrapEnabled) { throw new Error('Cannot specify "bootstrapOptions" if "bootstrapEnabled" is false'); } @@ -533,7 +537,7 @@ export class Cluster extends Resource implements ICluster { // do not attempt to map the role if `kubectl` is not enabled for this // cluster or if `mapRole` is set to false. By default this should happen. - const mapRole = options.mapRole === undefined ? true : options.mapRole; + const mapRole = options.mapRole ?? true; if (mapRole && this.kubectlEnabled) { // see https://docs.aws.amazon.com/en_us/eks/latest/userguide/add-user-role.html this.awsAuth.addRoleMapping(autoScalingGroup.role, { 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 9be42e435123c..a427353e9f6c7 100644 --- a/packages/@aws-cdk/aws-eks-legacy/lib/helm-chart.ts +++ b/packages/@aws-cdk/aws-eks-legacy/lib/helm-chart.ts @@ -1,10 +1,14 @@ import * as path from 'path'; import { CustomResource, CustomResourceProvider } from '@aws-cdk/aws-cloudformation'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Construct, Duration, Names, Stack } from '@aws-cdk/core'; +import { Duration, Names, Stack } from '@aws-cdk/core'; import { Cluster } from './cluster'; import { KubectlLayer } from './kubectl-layer'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Helm Chart options. */ diff --git a/packages/@aws-cdk/aws-eks-legacy/lib/k8s-resource.ts b/packages/@aws-cdk/aws-eks-legacy/lib/k8s-resource.ts index d5fd29d272903..14aa566d50406 100644 --- a/packages/@aws-cdk/aws-eks-legacy/lib/k8s-resource.ts +++ b/packages/@aws-cdk/aws-eks-legacy/lib/k8s-resource.ts @@ -1,7 +1,11 @@ import * as cfn from '@aws-cdk/aws-cloudformation'; -import { Construct, Stack } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { Cluster } from './cluster'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + export interface KubernetesResourceProps { /** * The EKS cluster to apply this configuration to. diff --git a/packages/@aws-cdk/aws-eks-legacy/lib/kubectl-layer.ts b/packages/@aws-cdk/aws-eks-legacy/lib/kubectl-layer.ts index 33aebc224d6ce..7121b34a277d6 100644 --- a/packages/@aws-cdk/aws-eks-legacy/lib/kubectl-layer.ts +++ b/packages/@aws-cdk/aws-eks-legacy/lib/kubectl-layer.ts @@ -1,6 +1,10 @@ import * as crypto from 'crypto'; import * as lambda from '@aws-cdk/aws-lambda'; -import { CfnResource, Construct, Resource, Stack, Token } from '@aws-cdk/core'; +import { CfnResource, Resource, Stack, Token } from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; const KUBECTL_APP_ARN = 'arn:aws:serverlessrepo:us-east-1:903779448426:applications/lambda-layer-kubectl'; const KUBECTL_APP_VERSION = '1.13.7'; diff --git a/packages/@aws-cdk/aws-eks-legacy/lib/user-data.ts b/packages/@aws-cdk/aws-eks-legacy/lib/user-data.ts index e889663520f32..75d6e009d657a 100644 --- a/packages/@aws-cdk/aws-eks-legacy/lib/user-data.ts +++ b/packages/@aws-cdk/aws-eks-legacy/lib/user-data.ts @@ -12,7 +12,7 @@ export function renderUserData(clusterName: string, autoScalingGroup: autoscalin const extraArgs = new Array(); - extraArgs.push(`--use-max-pods ${options.useMaxPods === undefined ? true : options.useMaxPods}`); + extraArgs.push(`--use-max-pods ${options.useMaxPods ?? true}`); if (options.awsApiRetryAttempts) { extraArgs.push(`--aws-api-retry-attempts ${options.awsApiRetryAttempts}`); diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 6737ac1c7f604..706a23720c157 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -1105,7 +1105,7 @@ export class Cluster extends ClusterBase { commonCommandOptions.push(`--role-arn ${mastersRole.roleArn}`); // allocate default capacity if non-zero (or default). - const minCapacity = props.defaultCapacity === undefined ? DEFAULT_CAPACITY_COUNT : props.defaultCapacity; + const minCapacity = props.defaultCapacity ?? DEFAULT_CAPACITY_COUNT; if (minCapacity > 0) { const instanceType = props.defaultCapacityInstance || DEFAULT_CAPACITY_TYPE; this.defaultCapacity = props.defaultCapacityType === DefaultCapacityType.EC2 ? @@ -1115,7 +1115,7 @@ export class Cluster extends ClusterBase { this.addNodegroupCapacity('DefaultCapacity', { instanceTypes: [instanceType], minSize: minCapacity }) : undefined; } - const outputConfigCommand = props.outputConfigCommand === undefined ? true : props.outputConfigCommand; + const outputConfigCommand = props.outputConfigCommand ?? true; if (outputConfigCommand) { const postfix = commonCommandOptions.join(' '); new CfnOutput(this, 'ConfigCommand', { value: `${updateConfigCommandPrefix} ${postfix}` }); @@ -1251,7 +1251,7 @@ export class Cluster extends ClusterBase { // allow traffic to/from managed node groups (eks attaches this security group to the managed nodes) autoScalingGroup.addSecurityGroup(this.clusterSecurityGroup); - const bootstrapEnabled = options.bootstrapEnabled !== undefined ? options.bootstrapEnabled : true; + const bootstrapEnabled = options.bootstrapEnabled ?? true; if (options.bootstrapOptions && !bootstrapEnabled) { throw new Error('Cannot specify "bootstrapOptions" if "bootstrapEnabled" is false'); } @@ -1278,7 +1278,7 @@ export class Cluster extends ClusterBase { // do not attempt to map the role if `kubectl` is not enabled for this // cluster or if `mapRole` is set to false. By default this should happen. - const mapRole = options.mapRole === undefined ? true : options.mapRole; + const mapRole = options.mapRole ?? true; if (mapRole) { // see https://docs.aws.amazon.com/en_us/eks/latest/userguide/add-user-role.html this.awsAuth.addRoleMapping(autoScalingGroup.role, { diff --git a/packages/@aws-cdk/aws-eks/lib/user-data.ts b/packages/@aws-cdk/aws-eks/lib/user-data.ts index cf38cf7ee9761..3b8d997535771 100644 --- a/packages/@aws-cdk/aws-eks/lib/user-data.ts +++ b/packages/@aws-cdk/aws-eks/lib/user-data.ts @@ -13,7 +13,7 @@ export function renderAmazonLinuxUserData(clusterName: string, autoScalingGroup: const extraArgs = new Array(); - extraArgs.push(`--use-max-pods ${options.useMaxPods === undefined ? true : options.useMaxPods}`); + extraArgs.push(`--use-max-pods ${options.useMaxPods ?? true}`); if (options.awsApiRetryAttempts) { extraArgs.push(`--aws-api-retry-attempts ${options.awsApiRetryAttempts}`); diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 939d90d267ac8..d602ef1e30c10 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -73,13 +73,13 @@ "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", "@types/yaml": "1.9.6", - "aws-sdk": "^2.830.0", + "aws-sdk": "^2.842.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0", - "sinon": "^9.2.1", + "sinon": "^9.2.4", "cdk8s-plus": "^0.33.0", "cdk8s": "^0.33.0" }, diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts index 55c423f157a55..eaed2daee58d2 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancing/lib/load-balancer.ts @@ -248,7 +248,7 @@ export class LoadBalancer extends Resource implements IConnectable { listeners: Lazy.any({ produce: () => this.listeners }), scheme: props.internetFacing ? 'internet-facing' : 'internal', healthCheck: props.healthCheck && healthCheckToJSON(props.healthCheck), - crossZone: (props.crossZone === undefined || props.crossZone) ? true : false, + crossZone: props.crossZone ?? true, }); if (props.internetFacing) { this.elb.node.addDependency(selectedSubnets.internetConnectivityEstablished); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-action.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-action.ts index 09ae095c46a92..91d839a605d74 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-action.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-action.ts @@ -1,9 +1,13 @@ -import { Construct, Duration, IConstruct, SecretValue, Tokenization } from '@aws-cdk/core'; +import { Duration, IConstruct, SecretValue, Tokenization } from '@aws-cdk/core'; import { CfnListener } from '../elasticloadbalancingv2.generated'; import { IListenerAction } from '../shared/listener-action'; import { IApplicationListener } from './application-listener'; import { IApplicationTargetGroup } from './application-target-group'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * What to do when a client makes a request to a listener * diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts index a457878c63bb9..a1d3de25bf82d 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts @@ -1,6 +1,6 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; -import { Annotations, Construct as CoreConstruct, Duration } from '@aws-cdk/core'; +import { Annotations, Duration } from '@aws-cdk/core'; import { IConstruct, Construct } from 'constructs'; import { ApplicationELBMetrics } from '../elasticloadbalancingv2-canned-metrics.generated'; import { @@ -13,6 +13,10 @@ import { determineProtocolAndPort } from '../shared/util'; import { IApplicationListener } from './application-listener'; import { HttpCodeTarget } from './application-load-balancer'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + /** * Properties for defining an Application Target Group */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener-action.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener-action.ts index 81b45a5e3b146..cc86ca0458ef7 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener-action.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener-action.ts @@ -1,9 +1,13 @@ -import { Construct, Duration } from '@aws-cdk/core'; +import { Duration } from '@aws-cdk/core'; import { CfnListener } from '../elasticloadbalancingv2.generated'; import { IListenerAction } from '../shared/listener-action'; import { INetworkListener } from './network-listener'; import { INetworkTargetGroup } from './network-target-group'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * What to do when a client makes a request to a listener * diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index 17170f4402b1a..db52671e5a368 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -108,7 +108,7 @@ export abstract class BaseListener extends Resource { const resource = new CfnListener(this, 'Resource', { ...additionalProps, - defaultActions: Lazy.any({ produce: () => this.defaultAction ? this.defaultAction.renderActions() : [] }), + defaultActions: Lazy.any({ produce: () => this.defaultAction?.renderActions() ?? [] }), }); this.listenerArn = resource.ref; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts index 0ff63805aab90..5e569fd8213ca 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -246,20 +246,20 @@ export abstract class TargetGroupBase extends CoreConstruct implements ITargetGr vpcId: cdk.Lazy.string({ produce: () => this.vpc && this.targetType !== TargetType.LAMBDA ? this.vpc.vpcId : undefined }), // HEALTH CHECK - healthCheckEnabled: cdk.Lazy.any({ produce: () => this.healthCheck && this.healthCheck.enabled }), + healthCheckEnabled: cdk.Lazy.any({ produce: () => this.healthCheck?.enabled }), healthCheckIntervalSeconds: cdk.Lazy.number({ - produce: () => this.healthCheck && this.healthCheck.interval && this.healthCheck.interval.toSeconds(), + produce: () => this.healthCheck?.interval?.toSeconds(), }), - healthCheckPath: cdk.Lazy.string({ produce: () => this.healthCheck && this.healthCheck.path }), - healthCheckPort: cdk.Lazy.string({ produce: () => this.healthCheck && this.healthCheck.port }), - healthCheckProtocol: cdk.Lazy.string({ produce: () => this.healthCheck && this.healthCheck.protocol }), + healthCheckPath: cdk.Lazy.string({ produce: () => this.healthCheck?.path }), + healthCheckPort: cdk.Lazy.string({ produce: () => this.healthCheck?.port }), + healthCheckProtocol: cdk.Lazy.string({ produce: () => this.healthCheck?.protocol }), healthCheckTimeoutSeconds: cdk.Lazy.number({ - produce: () => this.healthCheck && this.healthCheck.timeout && this.healthCheck.timeout.toSeconds(), + produce: () => this.healthCheck?.timeout?.toSeconds(), }), - healthyThresholdCount: cdk.Lazy.number({ produce: () => this.healthCheck && this.healthCheck.healthyThresholdCount }), - unhealthyThresholdCount: cdk.Lazy.number({ produce: () => this.healthCheck && this.healthCheck.unhealthyThresholdCount }), + healthyThresholdCount: cdk.Lazy.number({ produce: () => this.healthCheck?.healthyThresholdCount }), + unhealthyThresholdCount: cdk.Lazy.number({ produce: () => this.healthCheck?.unhealthyThresholdCount }), matcher: cdk.Lazy.any({ - produce: () => this.healthCheck && this.healthCheck.healthyHttpCodes !== undefined ? { + produce: () => this.healthCheck?.healthyHttpCodes !== undefined ? { httpCode: this.healthCheck.healthyHttpCodes, } : undefined, }), diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts index 89f2c88d7ad7e..be25c49b96513 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts @@ -82,6 +82,12 @@ export enum SslPolicy { */ RECOMMENDED = 'ELBSecurityPolicy-2016-08', + /** + * Strong foward secrecy ciphers and TLV1.2 only (2020 edition). + * Same as FORWARD_SECRECY_TLS12_RES, but only supports GCM versions of the TLS ciphers + */ + FORWARD_SECRECY_TLS12_RES_GCM = 'ELBSecurityPolicy-FS-1-2-Res-2020-10', + /** * Strong forward secrecy ciphers and TLS1.2 only */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts index 2ecc35c365694..a6d2692a59e83 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts @@ -67,7 +67,7 @@ export function determineProtocolAndPort(protocol: ApplicationProtocol | undefin * Helper function to default undefined input props */ export function ifUndefined(x: T | undefined, def: T) { - return x !== undefined ? x : def; + return x ?? def; } /** diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/security-group.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/security-group.test.ts index e4d45eb609335..67d14cd4f721e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/security-group.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/security-group.test.ts @@ -272,7 +272,7 @@ class TestFixture { }); this.lb = new elbv2.ApplicationLoadBalancer(this.stack, 'LB', { vpc: this.vpc }); - createListener = createListener === undefined ? true : createListener; + createListener = createListener ?? true; if (createListener) { this._listener = this.lb.addListener('Listener', { port: 80, open: false }); } diff --git a/packages/@aws-cdk/aws-elasticsearch/README.md b/packages/@aws-cdk/aws-elasticsearch/README.md index 35d3f63c6bf55..37d117c393616 100644 --- a/packages/@aws-cdk/aws-elasticsearch/README.md +++ b/packages/@aws-cdk/aws-elasticsearch/README.md @@ -225,3 +225,20 @@ const domain = new es.Domain(this, 'Domain', { }, }); ``` + +## Custom endpoint + +Custom endpoints can be configured to reach the ES domain under a custom domain name. + +```ts +new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_7, + customEndpoint: { + domainName: 'search.example.com', + }, +}); +``` + +It is also possible to specify a custom certificate instead of the auto-generated one. + +Additionally, an automatic CNAME-Record is created if a hosted zone is provided for the custom endpoint diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 603177c87c19f..28728e6cdf58f 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -1,10 +1,12 @@ import { URL } from 'url'; +import * as acm from '@aws-cdk/aws-certificatemanager'; import { Metric, MetricOptions, Statistic } from '@aws-cdk/aws-cloudwatch'; 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 logs from '@aws-cdk/aws-logs'; +import * as route53 from '@aws-cdk/aws-route53'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; @@ -395,6 +397,28 @@ export interface AdvancedSecurityOptions { readonly masterUserPassword?: cdk.SecretValue; } +/** + * Configures a custom domain endpoint for the ES domain + */ +export interface CustomEndpointOptions { + /** + * The custom domain name to assign + */ + readonly domainName: string; + + /** + * The certificate to use + * @default - create a new one + */ + readonly certificate?: acm.ICertificate; + + /** + * The hosted zone in Route53 to create the CNAME record in + * @default - do not create a CNAME + */ + readonly hostedZone?: route53.IHostedZone; +} + /** * Properties for an AWS Elasticsearch Domain. */ @@ -545,6 +569,13 @@ export interface DomainProps { */ readonly enableVersionUpgrade?: boolean; + /** + * To configure a custom domain configure these options + * + * If you specify a Route53 hosted zone it will create a CNAME record and use DNS validation for the certificate + * @default - no custom domain endpoint will be configured + */ + readonly customEndpoint?: CustomEndpointOptions; } /** @@ -1547,6 +1578,18 @@ export class Domain extends DomainBase implements IDomain { }; } + let customEndpointCertificate: acm.ICertificate | undefined; + if (props.customEndpoint) { + if (props.customEndpoint.certificate) { + customEndpointCertificate = props.customEndpoint.certificate; + } else { + customEndpointCertificate = new acm.Certificate(this, 'CustomEndpointCertificate', { + domainName: props.customEndpoint.domainName, + validation: props.customEndpoint.hostedZone ? acm.CertificateValidation.fromDns(props.customEndpoint.hostedZone) : undefined, + }); + } + } + // Create the domain this.domain = new CfnDomain(this, 'Resource', { domainName: this.physicalName, @@ -1602,6 +1645,11 @@ export class Domain extends DomainBase implements IDomain { domainEndpointOptions: { enforceHttps, tlsSecurityPolicy: props.tlsSecurityPolicy ?? TLSSecurityPolicy.TLS_1_0, + ...props.customEndpoint && { + customEndpointEnabled: true, + customEndpoint: props.customEndpoint.domainName, + customEndpointCertificateArn: customEndpointCertificate!.certificateArn, + }, }, advancedSecurityOptions: advancedSecurityEnabled ? { @@ -1637,6 +1685,14 @@ export class Domain extends DomainBase implements IDomain { resourceName: this.physicalName, }); + if (props.customEndpoint?.hostedZone) { + new route53.CnameRecord(this, 'CnameRecord', { + recordName: props.customEndpoint.domainName, + zone: props.customEndpoint.hostedZone, + domainName: this.domainEndpoint, + }); + } + const accessPolicyStatements: iam.PolicyStatement[] | undefined = unsignedBasicAuthEnabled ? (props.accessPolicies ?? []).concat(unsignedAccessPolicy) : props.accessPolicies; diff --git a/packages/@aws-cdk/aws-elasticsearch/package.json b/packages/@aws-cdk/aws-elasticsearch/package.json index c93f1397c06b8..89fa4f885f478 100644 --- a/packages/@aws-cdk/aws-elasticsearch/package.json +++ b/packages/@aws-cdk/aws-elasticsearch/package.json @@ -78,11 +78,13 @@ "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-route53": "0.0.0", "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/custom-resources": "0.0.0", "@aws-cdk/core": "0.0.0", @@ -90,11 +92,13 @@ }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-route53": "0.0.0", "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/custom-resources": "0.0.0", "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index f01fba3c4b332..98a6f350d6f34 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -1,11 +1,13 @@ /* eslint-disable jest/expect-expect */ import '@aws-cdk/assert/jest'; import * as assert from '@aws-cdk/assert'; +import * as acm from '@aws-cdk/aws-certificatemanager'; import { Metric, Statistic } from '@aws-cdk/aws-cloudwatch'; import { Subnet, Vpc, EbsDeviceVolumeType } from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; +import * as route53 from '@aws-cdk/aws-route53'; import { App, Stack, Duration, SecretValue } from '@aws-cdk/core'; import { Domain, ElasticsearchVersion } from '../lib'; @@ -987,6 +989,134 @@ describe('advanced security options', () => { }); }); +describe('custom endpoints', () => { + const customDomainName = 'search.example.com'; + + test('custom domain without hosted zone and default cert', () => { + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_1, + nodeToNodeEncryption: true, + enforceHttps: true, + customEndpoint: { + domainName: customDomainName, + }, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + DomainEndpointOptions: { + EnforceHTTPS: true, + CustomEndpointEnabled: true, + CustomEndpoint: customDomainName, + CustomEndpointCertificateArn: { + Ref: 'DomainCustomEndpointCertificateD080A69E', // Auto-generated certificate + }, + }, + }); + expect(stack).toHaveResourceLike('AWS::CertificateManager::Certificate', { + DomainName: customDomainName, + ValidationMethod: 'EMAIL', + }); + }); + + test('custom domain with hosted zone and default cert', () => { + const zone = new route53.HostedZone(stack, 'DummyZone', { zoneName: 'example.com' }); + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_1, + nodeToNodeEncryption: true, + enforceHttps: true, + customEndpoint: { + domainName: customDomainName, + hostedZone: zone, + }, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + DomainEndpointOptions: { + EnforceHTTPS: true, + CustomEndpointEnabled: true, + CustomEndpoint: customDomainName, + CustomEndpointCertificateArn: { + Ref: 'DomainCustomEndpointCertificateD080A69E', // Auto-generated certificate + }, + }, + }); + expect(stack).toHaveResourceLike('AWS::CertificateManager::Certificate', { + DomainName: customDomainName, + DomainValidationOptions: [ + { + DomainName: customDomainName, + HostedZoneId: { + Ref: 'DummyZone03E0FE81', + }, + }, + ], + ValidationMethod: 'DNS', + }); + expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', { + Name: 'search.example.com.', + Type: 'CNAME', + HostedZoneId: { + Ref: 'DummyZone03E0FE81', + }, + ResourceRecords: [ + { + 'Fn::GetAtt': [ + 'Domain66AC69E0', + 'DomainEndpoint', + ], + }, + ], + }); + }); + + test('custom domain with hosted zone and given cert', () => { + const zone = new route53.HostedZone(stack, 'DummyZone', { + zoneName: 'example.com', + }); + const certificate = new acm.Certificate(stack, 'DummyCert', { + domainName: customDomainName, + }); + + new Domain(stack, 'Domain', { + version: ElasticsearchVersion.V7_1, + nodeToNodeEncryption: true, + enforceHttps: true, + customEndpoint: { + domainName: customDomainName, + hostedZone: zone, + certificate, + }, + }); + + expect(stack).toHaveResourceLike('AWS::Elasticsearch::Domain', { + DomainEndpointOptions: { + EnforceHTTPS: true, + CustomEndpointEnabled: true, + CustomEndpoint: customDomainName, + CustomEndpointCertificateArn: { + Ref: 'DummyCertFA37670B', + }, + }, + }); + expect(stack).toHaveResourceLike('AWS::Route53::RecordSet', { + Name: 'search.example.com.', + Type: 'CNAME', + HostedZoneId: { + Ref: 'DummyZone03E0FE81', + }, + ResourceRecords: [ + { + 'Fn::GetAtt': [ + 'Domain66AC69E0', + 'DomainEndpoint', + ], + }, + ], + }); + }); + +}); + describe('custom error responses', () => { test('error when availabilityZoneCount does not match vpcOptions.subnets length', () => { diff --git a/packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts b/packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts index a4b19981dd0b1..baff4c76cde66 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts @@ -115,7 +115,7 @@ export class EcsTask implements events.IRuleTarget { this.cluster = props.cluster; this.taskDefinition = props.taskDefinition; - this.taskCount = props.taskCount !== undefined ? props.taskCount : 1; + this.taskCount = props.taskCount ?? 1; this.platformVersion = props.platformVersion; if (props.role) { diff --git a/packages/@aws-cdk/aws-events-targets/lib/util.ts b/packages/@aws-cdk/aws-events-targets/lib/util.ts index fe41154b6037c..74465558bb3f9 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/util.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/util.ts @@ -1,7 +1,11 @@ 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, ConstructNode, IConstruct, Names } from '@aws-cdk/core'; +import { ConstructNode, IConstruct, Names } from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; /** * Obtain the Role for the EventBridge event diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 2fbad9a4cd421..73998519c62a5 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -74,7 +74,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", - "aws-sdk": "^2.830.0", + "aws-sdk": "^2.842.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-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index 0079954e73558..969965021f75d 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -166,7 +166,7 @@ export class Rule extends Resource implements IRule { const targetProps = target.bind(this, autoGeneratedId); const inputProps = targetProps.input && targetProps.input.bind(this); - const roleArn = targetProps.role ? targetProps.role.roleArn : undefined; + const roleArn = targetProps.role?.roleArn; const id = targetProps.id || autoGeneratedId; if (targetProps.targetResource) { @@ -298,7 +298,7 @@ export class Rule extends Resource implements IRule { sqsParameters: targetProps.sqsParameters, input: inputProps && inputProps.input, inputPath: inputProps && inputProps.inputPath, - inputTransformer: inputProps && inputProps.inputTemplate !== undefined ? { + inputTransformer: inputProps?.inputTemplate !== undefined ? { inputTemplate: inputProps.inputTemplate, inputPathsMap: inputProps.inputPathsMap, } : undefined, diff --git a/packages/@aws-cdk/aws-events/lib/schedule.ts b/packages/@aws-cdk/aws-events/lib/schedule.ts index 1e0a391e2b911..8c2c04481c0d2 100644 --- a/packages/@aws-cdk/aws-events/lib/schedule.ts +++ b/packages/@aws-cdk/aws-events/lib/schedule.ts @@ -115,7 +115,7 @@ class LiteralSchedule extends Schedule { } function fallback(x: T | undefined, def: T): T { - return x === undefined ? def : x; + return x ?? def; } /** diff --git a/packages/@aws-cdk/aws-globalaccelerator/lib/accelerator-security-group.ts b/packages/@aws-cdk/aws-globalaccelerator/lib/accelerator-security-group.ts index 793ae6b3aceca..9197613d69b61 100644 --- a/packages/@aws-cdk/aws-globalaccelerator/lib/accelerator-security-group.ts +++ b/packages/@aws-cdk/aws-globalaccelerator/lib/accelerator-security-group.ts @@ -1,8 +1,12 @@ import { ISecurityGroup, SecurityGroup, IVpc } from '@aws-cdk/aws-ec2'; -import { Construct } from '@aws-cdk/core'; + import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from '@aws-cdk/custom-resources'; import { EndpointGroup } from '../lib'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * The security group used by a Global Accelerator to send traffic to resources in a VPC. */ diff --git a/packages/@aws-cdk/aws-glue/lib/table.ts b/packages/@aws-cdk/aws-glue/lib/table.ts index 859bb0b2ee325..635fe67a68d35 100644 --- a/packages/@aws-cdk/aws-glue/lib/table.ts +++ b/packages/@aws-cdk/aws-glue/lib/table.ts @@ -243,7 +243,7 @@ export class Table extends Resource implements ITable { this.columns = props.columns; this.partitionKeys = props.partitionKeys; - this.compressed = props.compressed === undefined ? false : props.compressed; + this.compressed = props.compressed ?? false; const { bucket, encryption, encryptionKey } = createBucket(this, props); this.bucket = bucket; this.encryption = encryption; @@ -267,7 +267,7 @@ export class Table extends Resource implements ITable { storageDescriptor: { location: `s3://${this.bucket.bucketName}/${this.s3Prefix}`, compressed: this.compressed, - storedAsSubDirectories: props.storedAsSubDirectories === undefined ? false : props.storedAsSubDirectories, + storedAsSubDirectories: props.storedAsSubDirectories ?? false, columns: renderColumns(props.columns), inputFormat: props.dataFormat.inputFormat.className, outputFormat: props.dataFormat.outputFormat.className, diff --git a/packages/@aws-cdk/aws-glue/test/table.test.ts b/packages/@aws-cdk/aws-glue/test/table.test.ts index ec92e7ced4ff2..0969088346491 100644 --- a/packages/@aws-cdk/aws-glue/test/table.test.ts +++ b/packages/@aws-cdk/aws-glue/test/table.test.ts @@ -345,50 +345,6 @@ test('encrypted table: SSE-KMS (implicitly created key)', () => { equal(table.encryptionKey, table.bucket.encryptionKey); cdkExpect(stack).to(haveResource('AWS::KMS::Key', { - KeyPolicy: { - Statement: [ - { - Action: [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', - ], - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Resource: '*', - }, - ], - Version: '2012-10-17', - }, Description: 'Created by Default/Table/Bucket', })); @@ -462,7 +418,9 @@ test('encrypted table: SSE-KMS (explicitly created key)', () => { const database = new glue.Database(stack, 'Database', { databaseName: 'database', }); - const encryptionKey = new kms.Key(stack, 'MyKey'); + const encryptionKey = new kms.Key(stack, 'MyKey', { + description: 'OurKey', + }); const table = new glue.Table(stack, 'Table', { database, @@ -480,50 +438,7 @@ test('encrypted table: SSE-KMS (explicitly created key)', () => { notEqual(table.encryptionKey, undefined); cdkExpect(stack).to(haveResource('AWS::KMS::Key', { - KeyPolicy: { - Statement: [ - { - Action: [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', - ], - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Resource: '*', - }, - ], - Version: '2012-10-17', - }, + Description: 'OurKey', })); cdkExpect(stack).to(haveResource('AWS::S3::Bucket', { @@ -690,52 +605,7 @@ test('encrypted table: CSE-KMS (implicitly created key)', () => { notEqual(table.encryptionKey, undefined); equal(table.bucket.encryptionKey, undefined); - cdkExpect(stack).to(haveResource('AWS::KMS::Key', { - KeyPolicy: { - Statement: [ - { - Action: [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', - ], - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Resource: '*', - }, - ], - Version: '2012-10-17', - }, - })); + cdkExpect(stack).to(haveResource('AWS::KMS::Key')); cdkExpect(stack).to(haveResource('AWS::Glue::Table', { CatalogId: { @@ -789,7 +659,9 @@ test('encrypted table: CSE-KMS (explicitly created key)', () => { const database = new glue.Database(stack, 'Database', { databaseName: 'database', }); - const encryptionKey = new kms.Key(stack, 'MyKey'); + const encryptionKey = new kms.Key(stack, 'MyKey', { + description: 'MyKey', + }); const table = new glue.Table(stack, 'Table', { database, @@ -807,50 +679,7 @@ test('encrypted table: CSE-KMS (explicitly created key)', () => { equal(table.bucket.encryptionKey, undefined); cdkExpect(stack).to(haveResource('AWS::KMS::Key', { - KeyPolicy: { - Statement: [ - { - Action: [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', - ], - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Resource: '*', - }, - ], - Version: '2012-10-17', - }, + Description: 'MyKey', })); cdkExpect(stack).to(haveResource('AWS::Glue::Table', { @@ -906,7 +735,9 @@ test('encrypted table: CSE-KMS (explicitly passed bucket and key)', () => { databaseName: 'database', }); const bucket = new s3.Bucket(stack, 'Bucket'); - const encryptionKey = new kms.Key(stack, 'MyKey'); + const encryptionKey = new kms.Key(stack, 'MyKey', { + description: 'MyKey', + }); const table = new glue.Table(stack, 'Table', { database, @@ -925,50 +756,7 @@ test('encrypted table: CSE-KMS (explicitly passed bucket and key)', () => { equal(table.bucket.encryptionKey, undefined); cdkExpect(stack).to(haveResource('AWS::KMS::Key', { - KeyPolicy: { - Statement: [ - { - Action: [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', - ], - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Resource: '*', - }, - ], - Version: '2012-10-17', - }, + Description: 'MyKey', })); cdkExpect(stack).to(haveResource('AWS::Glue::Table', { diff --git a/packages/@aws-cdk/aws-iam/README.md b/packages/@aws-cdk/aws-iam/README.md index d9488e7d081c8..f2b86ccff2f59 100644 --- a/packages/@aws-cdk/aws-iam/README.md +++ b/packages/@aws-cdk/aws-iam/README.md @@ -264,6 +264,50 @@ const newPolicy = new Policy(stack, 'MyNewPolicy', { }); ``` +## Permissions Boundaries + +[Permissions +Boundaries](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) +can be used as a mechanism to prevent privilege esclation by creating new +`Role`s. Permissions Boundaries are a Managed Policy, attached to Roles or +Users, that represent the *maximum* set of permissions they can have. The +effective set of permissions of a Role (or User) will be the intersection of +the Identity Policy and the Permissions Boundary attached to the Role (or +User). Permissions Boundaries are typically created by account +Administrators, and their use on newly created `Role`s will be enforced by +IAM policies. + +It is possible to attach Permissions Boundaries to all Roles created in a construct +tree all at once: + +```ts +// This imports an existing policy. +const boundary = iam.ManagedPolicy.fromManagedPolicyArn(this, 'Boundary', 'arn:aws:iam::123456789012:policy/boundary'); + +// This creates a new boundary +const boundary2 = new iam.ManagedPolicy(this, 'Boundary2', { + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.DENY, + actions: ['iam:*'], + resources: ['*'], + }), + ], +}); + +// Directly apply the boundary to a Role you create +iam.PermissionsBoundary.of(role).apply(boundary); + +// Apply the boundary to an Role that was implicitly created for you +iam.PermissionsBoundary.of(lambdaFunction).apply(boundary); + +// Apply the boundary to all Roles in a stack +iam.PermissionsBoundary.of(stack).apply(boundary); + +// Remove a Permissions Boundary that is inherited, for example from the Stack level +iam.PermissionsBoundary.of(customResource).clear(); +``` + ## OpenID Connect Providers OIDC identity providers are entities in IAM that describe an external identity diff --git a/packages/@aws-cdk/aws-iam/lib/index.ts b/packages/@aws-cdk/aws-iam/lib/index.ts index ba9250ca1e08e..19b8a156ba598 100644 --- a/packages/@aws-cdk/aws-iam/lib/index.ts +++ b/packages/@aws-cdk/aws-iam/lib/index.ts @@ -11,6 +11,7 @@ export * from './identity-base'; export * from './grant'; export * from './unknown-principal'; export * from './oidc-provider'; +export * from './permissions-boundary'; // AWS::IAM CloudFormation Resources: export * from './iam.generated'; diff --git a/packages/@aws-cdk/aws-iam/lib/permissions-boundary.ts b/packages/@aws-cdk/aws-iam/lib/permissions-boundary.ts new file mode 100644 index 0000000000000..7b4320462a1ac --- /dev/null +++ b/packages/@aws-cdk/aws-iam/lib/permissions-boundary.ts @@ -0,0 +1,53 @@ +import { Node, IConstruct } from 'constructs'; +import { CfnRole, CfnUser } from './iam.generated'; +import { IManagedPolicy } from './managed-policy'; + +/** + * Modify the Permissions Boundaries of Users and Roles in a construct tree + * + * @example + * + * const policy = ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess'); + * PermissionsBoundary.of(stack).apply(policy); + */ +export class PermissionsBoundary { + /** + * Access the Permissions Boundaries of a construct tree + */ + public static of(scope: IConstruct): PermissionsBoundary { + return new PermissionsBoundary(scope); + } + + private constructor(private readonly scope: IConstruct) { + } + + /** + * Apply the given policy as Permissions Boundary to all Roles in the scope + * + * Will override any Permissions Boundaries configured previously; in case + * a Permission Boundary is applied in multiple scopes, the Boundary applied + * closest to the Role wins. + */ + public apply(boundaryPolicy: IManagedPolicy) { + Node.of(this.scope).applyAspect({ + visit(node: IConstruct) { + if (node instanceof CfnRole || node instanceof CfnUser) { + node.permissionsBoundary = boundaryPolicy.managedPolicyArn; + } + }, + }); + } + + /** + * Remove previously applied Permissions Boundaries + */ + public clear() { + Node.of(this.scope).applyAspect({ + visit(node: IConstruct) { + if (node instanceof CfnRole || node instanceof CfnUser) { + node.permissionsBoundary = undefined; + } + }, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/lib/policy.ts b/packages/@aws-cdk/aws-iam/lib/policy.ts index 795049a1cc163..60862dca07c56 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy.ts @@ -160,7 +160,7 @@ export class Policy extends Resource implements IPolicy { }); this._policyName = this.physicalName!; - this.force = props.force !== undefined ? props.force : false; + this.force = props.force ?? false; if (props.users) { props.users.forEach(u => this.attachToUser(u)); diff --git a/packages/@aws-cdk/aws-iam/lib/role.ts b/packages/@aws-cdk/aws-iam/lib/role.ts index 74e251bd7bc07..9b81e4f174152 100644 --- a/packages/@aws-cdk/aws-iam/lib/role.ts +++ b/packages/@aws-cdk/aws-iam/lib/role.ts @@ -1,4 +1,4 @@ -import { Duration, Lazy, Resource, Stack, Token, TokenComparison } from '@aws-cdk/core'; +import { Duration, Resource, Stack, Token, TokenComparison } from '@aws-cdk/core'; import { Construct, Node } from 'constructs'; import { Grant } from './grant'; import { CfnRole } from './iam.generated'; @@ -9,7 +9,7 @@ import { PolicyDocument } from './policy-document'; import { PolicyStatement } from './policy-statement'; import { AddToPrincipalPolicyResult, ArnPrincipal, IPrincipal, PrincipalPolicyFragment } from './principals'; import { ImmutableRole } from './private/immutable-role'; -import { AttachedPolicies } from './util'; +import { AttachedPolicies, UniqueStringSet } from './util'; /** * Properties for defining an IAM Role @@ -326,7 +326,7 @@ export class Role extends Resource implements IRole { const role = new CfnRole(this, 'Resource', { assumeRolePolicyDocument: this.assumeRolePolicy as any, - managedPolicyArns: Lazy.list({ produce: () => this.managedPolicies.map(p => p.managedPolicyArn) }, { omitEmpty: true }), + managedPolicyArns: UniqueStringSet.from(() => this.managedPolicies.map(p => p.managedPolicyArn)), policies: _flatten(this.inlinePolicies), path: props.path, permissionsBoundary: this.permissionsBoundary ? this.permissionsBoundary.managedPolicyArn : undefined, diff --git a/packages/@aws-cdk/aws-iam/lib/util.ts b/packages/@aws-cdk/aws-iam/lib/util.ts index b5f1700baefe7..19fcbffe09639 100644 --- a/packages/@aws-cdk/aws-iam/lib/util.ts +++ b/packages/@aws-cdk/aws-iam/lib/util.ts @@ -1,4 +1,4 @@ -import { DefaultTokenResolver, Lazy, StringConcat, Tokenization } from '@aws-cdk/core'; +import { captureStackTrace, DefaultTokenResolver, IPostProcessor, IResolvable, IResolveContext, Lazy, StringConcat, Token, Tokenization } from '@aws-cdk/core'; import { IConstruct } from 'constructs'; import { IPolicy } from './policy'; @@ -82,3 +82,45 @@ export function mergePrincipal(target: { [key: string]: string[] }, source: { [k return target; } + +/** + * Lazy string set token that dedupes entries + * + * Needs to operate post-resolve, because the inputs could be + * `[ '${Token[TOKEN.9]}', '${Token[TOKEN.10]}', '${Token[TOKEN.20]}' ]`, which + * still all resolve to the same string value. + * + * Needs to JSON.stringify() results because strings could resolve to literal + * strings but could also resolve to `{ Fn::Join: [...] }`. + */ +export class UniqueStringSet implements IResolvable, IPostProcessor { + public static from(fn: () => string[]) { + return Token.asList(new UniqueStringSet(fn)); + } + + public readonly creationStack: string[]; + + private constructor(private readonly fn: () => string[]) { + this.creationStack = captureStackTrace(); + } + + public resolve(context: IResolveContext) { + context.registerPostProcessor(this); + return this.fn(); + } + + public postProcess(input: any, _context: IResolveContext) { + if (!Array.isArray(input)) { return input; } + if (input.length === 0) { return undefined; } + + const uniq: Record = {}; + for (const el of input) { + uniq[JSON.stringify(el)] = el; + } + return Object.values(uniq); + } + + public toString(): string { + return Token.asString(this); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/package.json b/packages/@aws-cdk/aws-iam/package.json index b34b959da8488..2500a54acd9b9 100644 --- a/packages/@aws-cdk/aws-iam/package.json +++ b/packages/@aws-cdk/aws-iam/package.json @@ -84,7 +84,7 @@ "cfn2ts": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0", - "sinon": "^9.2.1" + "sinon": "^9.2.4" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-iam/test/immutable-role.test.ts b/packages/@aws-cdk/aws-iam/test/immutable-role.test.ts index 0a14afee947a5..343c437c494e4 100644 --- a/packages/@aws-cdk/aws-iam/test/immutable-role.test.ts +++ b/packages/@aws-cdk/aws-iam/test/immutable-role.test.ts @@ -1,7 +1,11 @@ import '@aws-cdk/assert/jest'; -import { Construct, Stack } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import * as iam from '../lib'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /* eslint-disable quote-props */ describe('ImmutableRole', () => { diff --git a/packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts b/packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts new file mode 100644 index 0000000000000..99a14de55f2e1 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/permissions-boundary.test.ts @@ -0,0 +1,101 @@ +import { ABSENT } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { App, Stack } from '@aws-cdk/core'; +import * as iam from '../lib'; + +let app: App; +let stack: Stack; +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack'); +}); + +test('apply imported boundary to a role', () => { + // GIVEN + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('service.amazonaws.com'), + }); + + // WHEN + iam.PermissionsBoundary.of(role).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); + + // THEN + expect(stack).toHaveResource('AWS::IAM::Role', { + PermissionsBoundary: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/ReadOnlyAccess', + ]], + }, + }); +}); + +test('apply imported boundary to a user', () => { + // GIVEN + const user = new iam.User(stack, 'User'); + + // WHEN + iam.PermissionsBoundary.of(user).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); + + // THEN + expect(stack).toHaveResource('AWS::IAM::User', { + PermissionsBoundary: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/ReadOnlyAccess', + ]], + }, + }); +}); + +test('apply newly created boundary to a role', () => { + // GIVEN + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('service.amazonaws.com'), + }); + + // WHEN + iam.PermissionsBoundary.of(role).apply(new iam.ManagedPolicy(stack, 'Policy', { + statements: [ + new iam.PolicyStatement({ + actions: ['*'], + resources: ['*'], + }), + ], + })); + + // THEN + expect(stack).toHaveResource('AWS::IAM::Role', { + PermissionsBoundary: { Ref: 'Policy23B91518' }, + }); +}); + +test('unapply inherited boundary from a user: order 1', () => { + // GIVEN + const user = new iam.User(stack, 'User'); + + // WHEN + iam.PermissionsBoundary.of(stack).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); + iam.PermissionsBoundary.of(user).clear(); + + // THEN + expect(stack).toHaveResource('AWS::IAM::User', { + PermissionsBoundary: ABSENT, + }); +}); + +test('unapply inherited boundary from a user: order 2', () => { + // GIVEN + const user = new iam.User(stack, 'User'); + + // WHEN + iam.PermissionsBoundary.of(user).clear(); + iam.PermissionsBoundary.of(stack).apply(iam.ManagedPolicy.fromAwsManagedPolicyName('ReadOnlyAccess')); + + // THEN + expect(stack).toHaveResource('AWS::IAM::User', { + PermissionsBoundary: ABSENT, + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/role.test.ts b/packages/@aws-cdk/aws-iam/test/role.test.ts index 510416f580f39..8f0415c45598d 100644 --- a/packages/@aws-cdk/aws-iam/test/role.test.ts +++ b/packages/@aws-cdk/aws-iam/test/role.test.ts @@ -535,3 +535,31 @@ describe('IAM role', () => { expect(() => app.synth()).toThrow(/A PolicyStatement used in a resource-based policy must specify at least one IAM principal/); }); }); + +test('managed policy ARNs are deduplicated', () => { + const app = new App(); + const stack = new Stack(app, 'my-stack'); + const role = new Role(stack, 'MyRole', { + assumedBy: new ServicePrincipal('sns.amazonaws.com'), + managedPolicies: [ + ManagedPolicy.fromAwsManagedPolicyName('SuperDeveloper'), + ManagedPolicy.fromAwsManagedPolicyName('SuperDeveloper'), + ], + }); + role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('SuperDeveloper')); + + expect(stack).toHaveResource('AWS::IAM::Role', { + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iam::aws:policy/SuperDeveloper', + ], + ], + }, + ], + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kinesis/test/stream.test.ts b/packages/@aws-cdk/aws-kinesis/test/stream.test.ts index b9bcf8b92bca8..9c3cdc1eb7125 100644 --- a/packages/@aws-cdk/aws-kinesis/test/stream.test.ts +++ b/packages/@aws-cdk/aws-kinesis/test/stream.test.ts @@ -1,7 +1,9 @@ import '@aws-cdk/assert/jest'; +import { arrayWith } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import { App, Duration, Stack, CfnParameter } from '@aws-cdk/core'; +import { testFutureBehavior, testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import { Stream, StreamEncryption } from '../lib'; /* eslint-disable quote-props */ @@ -329,77 +331,18 @@ describe('Kinesis data streams', () => { test('auto-creates KMS key if encryption type is KMS but no key is provided', () => { const stack = new Stack(); - new Stream(stack, 'MyStream', { + const stream = new Stream(stack, 'MyStream', { encryption: StreamEncryption.KMS, }); - expect(stack).toMatchTemplate({ - Resources: { - MyStreamKey76F3300E: { - Type: 'AWS::KMS::Key', - Properties: { - Description: 'Created by Default/MyStream', - KeyPolicy: { - Statement: [ - { - Action: [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', - ], - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Resource: '*', - }, - ], - Version: '2012-10-17', - }, - }, - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', - }, - MyStream5C050E93: { - Type: 'AWS::Kinesis::Stream', - Properties: { - RetentionPeriodHours: 24, - ShardCount: 1, - StreamEncryption: { - EncryptionType: 'KMS', - KeyId: { - 'Fn::GetAtt': ['MyStreamKey76F3300E', 'Arn'], - }, - }, - }, - }, + expect(stack).toHaveResource('AWS::KMS::Key', { + Description: 'Created by Default/MyStream', + }); + + expect(stack).toHaveResource('AWS::Kinesis::Stream', { + StreamEncryption: { + EncryptionType: 'KMS', + KeyId: stack.resolve(stream.encryptionKey?.keyArn), }, }); }), @@ -416,78 +359,21 @@ describe('Kinesis data streams', () => { encryptionKey: explicitKey, }); - expect(stack).toMatchTemplate({ - Resources: { - ExplicitKey7DF42F37: { - Type: 'AWS::KMS::Key', - Properties: { - Description: 'Explicit Key', - KeyPolicy: { - Statement: [ - { - Action: [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', - ], - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Resource: '*', - }, - ], - Version: '2012-10-17', - }, - }, - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', - }, - MyStream5C050E93: { - Type: 'AWS::Kinesis::Stream', - Properties: { - RetentionPeriodHours: 24, - ShardCount: 1, - StreamEncryption: { - EncryptionType: 'KMS', - KeyId: { - 'Fn::GetAtt': ['ExplicitKey7DF42F37', 'Arn'], - }, - }, - }, - }, + expect(stack).toHaveResource('AWS::KMS::Key', { + Description: 'Explicit Key', + }); + + expect(stack).toHaveResource('AWS::Kinesis::Stream', { + RetentionPeriodHours: 24, + ShardCount: 1, + StreamEncryption: { + EncryptionType: 'KMS', + KeyId: stack.resolve(explicitKey.keyArn), }, }); }), - test('grantRead creates and attaches a policy with read only access to Stream and EncryptionKey', () => { + test('grantRead creates and attaches a policy with read only access to the principal', () => { const stack = new Stack(); const stream = new Stream(stack, 'MyStream', { encryption: StreamEncryption.KMS, @@ -496,6 +382,34 @@ describe('Kinesis data streams', () => { const user = new iam.User(stack, 'MyUser'); stream.grantRead(user); + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith({ + Action: 'kms:Decrypt', + Effect: 'Allow', + Resource: stack.resolve(stream.encryptionKey?.keyArn), + }), + }, + }); + + expect(stack).toHaveResourceLike('AWS::Kinesis::Stream', { + StreamEncryption: { + KeyId: stack.resolve(stream.encryptionKey?.keyArn), + }, + }); + }); + + // only applicable to legacy behaviour + // With the '@aws-cdk/aws-kms:defaultKeyPolicies' feature flag, KMS key policy is not updated. + testLegacyBehavior('grantRead creates and attaches a policy with read only access to EncryptionKey', App, (app) => { + const stack = new Stack(app); + const stream = new Stream(stack, 'MyStream', { + encryption: StreamEncryption.KMS, + }); + + const user = new iam.User(stack, 'MyUser'); + stream.grantRead(user); + expect(stack).toMatchTemplate({ Resources: { MyStreamKey76F3300E: { @@ -616,7 +530,7 @@ describe('Kinesis data streams', () => { }); }), - test('grantWrite creates and attaches a policy with write only access to Stream and EncryptionKey', () => { + test('grantWrite creates and attaches a policy with write only access to the principal', () => { const stack = new Stack(); const stream = new Stream(stack, 'MyStream', { encryption: StreamEncryption.KMS, @@ -625,6 +539,34 @@ describe('Kinesis data streams', () => { const user = new iam.User(stack, 'MyUser'); stream.grantWrite(user); + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith({ + Action: ['kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: stack.resolve(stream.encryptionKey?.keyArn), + }), + }, + }); + + expect(stack).toHaveResourceLike('AWS::Kinesis::Stream', { + StreamEncryption: { + KeyId: stack.resolve(stream.encryptionKey?.keyArn), + }, + }); + }); + + // only applicable to legacy behaviour + // With the '@aws-cdk/aws-kms:defaultKeyPolicies' feature flag, KMS key policy is not updated. + testLegacyBehavior('grantWrite creates and attaches a policy with write only access to EncryptionKey', App, (app) => { + const stack = new Stack(app); + const stream = new Stream(stack, 'MyStream', { + encryption: StreamEncryption.KMS, + }); + + const user = new iam.User(stack, 'MyUser'); + stream.grantWrite(user); + expect(stack).toMatchTemplate({ Resources: { MyStreamKey76F3300E: { @@ -739,7 +681,7 @@ describe('Kinesis data streams', () => { }); }), - test('grantReadWrite creates and attaches a policy with access to Stream and EncryptionKey', () => { + test('grantReadWrite creates and attaches a policy to the principal', () => { const stack = new Stack(); const stream = new Stream(stack, 'MyStream', { encryption: StreamEncryption.KMS, @@ -748,6 +690,34 @@ describe('Kinesis data streams', () => { const user = new iam.User(stack, 'MyUser'); stream.grantReadWrite(user); + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith({ + Action: ['kms:Decrypt', 'kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], + Effect: 'Allow', + Resource: stack.resolve(stream.encryptionKey?.keyArn), + }), + }, + }); + + expect(stack).toHaveResourceLike('AWS::Kinesis::Stream', { + StreamEncryption: { + KeyId: stack.resolve(stream.encryptionKey?.keyArn), + }, + }); + }); + + // only applicable to legacy behaviour + // With the '@aws-cdk/aws-kms:defaultKeyPolicies' feature flag, KMS key policy is not updated. + testLegacyBehavior('grantReadWrite creates and attaches a policy with access to EncryptionKey', App, (app) => { + const stack = new Stack(app); + const stream = new Stream(stack, 'MyStream', { + encryption: StreamEncryption.KMS, + }); + + const user = new iam.User(stack, 'MyUser'); + stream.grantReadWrite(user); + expect(stack).toMatchTemplate({ Resources: { MyStreamKey76F3300E: { @@ -1270,8 +1240,8 @@ describe('Kinesis data streams', () => { }); }), - test('fails with encryption due to cyclic dependency', () => { - const app = new App(); + // legacy behaviour as this is fixed with the feature flag. see subsequent test. + testLegacyBehavior('fails with encryption due to cyclic dependency', App, (app) => { const stackA = new Stack(app, 'stackA'); const streamFromStackA = new Stream(stackA, 'MyStream', { encryption: StreamEncryption.KMS, @@ -1285,6 +1255,29 @@ describe('Kinesis data streams', () => { }).toThrow(/'stack.' depends on 'stack.'/); }); + testFutureBehavior('cross stack permissions - with encryption', { '@aws-cdk/aws-kms:defaultKeyPolicies': true }, App, (app) => { + const stackA = new Stack(app, 'stackA'); + const streamFromStackA = new Stream(stackA, 'MyStream', { + encryption: StreamEncryption.KMS, + }); + + const stackB = new Stack(app, 'stackB'); + const user = new iam.User(stackB, 'UserWhoNeedsAccess'); + streamFromStackA.grantRead(user); + + expect(stackB).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith({ + Action: 'kms:Decrypt', + Effect: 'Allow', + Resource: { + 'Fn::ImportValue': 'stackA:ExportsOutputFnGetAttMyStreamKey76F3300EArn190947B4', + }, + }), + }, + }); + }); + test('accepts if retentionPeriodHours is a Token', () => { const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/.eslintrc.js b/packages/@aws-cdk/aws-kinesisanalytics-flink/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/.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-kinesisanalytics-flink/.gitignore b/packages/@aws-cdk/aws-kinesisanalytics-flink/.gitignore new file mode 100644 index 0000000000000..d8a8561d50885 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk +!.eslintrc.js +!jest.config.js + +junit.xml \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/.npmignore b/packages/@aws-cdk/aws-kinesisanalytics-flink/.npmignore new file mode 100644 index 0000000000000..63ab95621c764 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/.npmignore @@ -0,0 +1,27 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json +.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-kinesisanalytics-flink/LICENSE b/packages/@aws-cdk/aws-kinesisanalytics-flink/LICENSE new file mode 100644 index 0000000000000..28e4bdcec77ec --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/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-2021 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-kinesisanalytics-flink/NOTICE b/packages/@aws-cdk/aws-kinesisanalytics-flink/NOTICE new file mode 100644 index 0000000000000..5fc3826926b5b --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/README.md b/packages/@aws-cdk/aws-kinesisanalytics-flink/README.md new file mode 100644 index 0000000000000..7dc9b1a088b16 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/README.md @@ -0,0 +1,74 @@ +# Kinesis Analytics Flink + + +--- + +![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. + +--- + + + +This package provides constructs for creating Kinesis Analytics Flink +applications. To learn more about using using managed Flink applications, see +the [AWS developer +guide](https://docs.aws.amazon.com/kinesisanalytics/latest/java/what-is.html). + +## Creating Flink Applications + +To create a new Flink application, use the `Application` construct: + +[simple flink application](test/integ.application.lit.ts) + +The `code` property can use `fromAsset` as shown above to reference a local jar +file in s3 or `fromBucket` to reference a file in s3. + +[flink application using code from bucket](test/integ.application-code-from-bucket.lit.ts) + +The `propertyGroups` property provides a way of passing arbitrary runtime +properties to your Flink application. You can use the +aws-kinesisanalytics-runtime library to [retrieve these +properties](https://docs.aws.amazon.com/kinesisanalytics/latest/java/how-properties.html#how-properties-access). + +```ts +import * as flink from '@aws-cdk/aws-kinesisanalytics-flink'; + +const flinkApp = new flink.Application(this, 'Application', { + // ... + propertyGroups: { + FlinkApplicationProperties: { + inputStreamName: 'my-input-kinesis-stream', + outputStreamName: 'my-output-kinesis-stream', + }, + }, +}); +``` + +Flink applications also have specific configuration for passing parameters +when the Flink job starts. These include parameters for checkpointing, +snapshotting, monitoring, and parallelism. + +```ts +import * as logs from '@aws-cdk/aws-logs'; + +const flinkApp = new flink.Application(this, 'Application', { + code: flink.ApplicationCode.fromBucket(bucket, 'my-app.jar'), + runtime: file.Runtime.FLINK_1_11, + checkpointingEnabled: true, // default is true + checkpointInterval: cdk.Duration.seconds(30), // default is 1 minute + minPausesBetweenCheckpoints: cdk.Duration.seconds(10), // default is 5 seconds + logLevel: flink.LogLevel.ERROR, // default is INFO + metricsLevel: flink.MetricsLevel.PARALLELISM, // default is APPLICATION + autoScalingEnabled: false, // default is true + parallelism: 32, // default is 1 + parallelismPerKpu: 2, // default is 1 + snapshotsEnabled: false, // default is true + logGroup: new logs.LogGroup(this, 'LogGroup'), // by default, a new LogGroup will be created +}); +``` diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/jest.config.js b/packages/@aws-cdk/aws-kinesisanalytics-flink/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/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-kinesisanalytics-flink/lib/application-code.ts b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/application-code.ts new file mode 100644 index 0000000000000..c7d7b49180086 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/application-code.ts @@ -0,0 +1,139 @@ +import * as ka from '@aws-cdk/aws-kinesisanalytics'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import { Construct } from '@aws-cdk/core'; + +/** + * The return type of {@link ApplicationCode.bind}. This represents + * CloudFormation configuration and an s3 bucket holding the Flink application + * JAR file. + */ +export interface ApplicationCodeConfig { + /** + * Low-level Cloudformation ApplicationConfigurationProperty + */ + readonly applicationCodeConfigurationProperty: ka.CfnApplicationV2.ApplicationConfigurationProperty; + + /** + * S3 Bucket that stores the Flink application code + */ + readonly bucket: s3.IBucket; +} + +/** + * Code configuration providing the location to a Flink application JAR file. + */ +export abstract class ApplicationCode { + /** + * Reference code from an S3 bucket. + * + * @param bucket - an s3 bucket + * @param fileKey - a key pointing to a Flink JAR file + * @param objectVersion - an optional version string for the provided fileKey + */ + public static fromBucket(bucket: s3.IBucket, fileKey: string, objectVersion?: string): ApplicationCode { + return new BucketApplicationCode({ + bucket, + fileKey, + objectVersion, + }); + } + + /** + * Reference code from a local directory containing a Flink JAR file. + * + * @param path - a local directory path + * @parm options - standard s3 AssetOptions + */ + public static fromAsset(path: string, options?: s3_assets.AssetOptions): ApplicationCode { + return new AssetApplicationCode(path, options); + } + + /** + * A method to lazily bind asset resources to the parent FlinkApplication. + */ + public abstract bind(scope: Construct): ApplicationCodeConfig; +} + +interface BucketApplicationCodeProps { + readonly bucket: s3.IBucket; + readonly fileKey: string; + readonly objectVersion?: string; +} + +class BucketApplicationCode extends ApplicationCode { + public readonly bucket?: s3.IBucket; + public readonly fileKey: string; + public readonly objectVersion?: string; + + constructor(props: BucketApplicationCodeProps) { + super(); + this.bucket = props.bucket; + this.fileKey = props.fileKey; + this.objectVersion = props.objectVersion; + } + + public bind(_scope: Construct): ApplicationCodeConfig { + return { + applicationCodeConfigurationProperty: { + applicationCodeConfiguration: { + codeContent: { + s3ContentLocation: { + bucketArn: this.bucket!.bucketArn, + fileKey: this.fileKey, + objectVersion: this.objectVersion, + }, + }, + codeContentType: 'ZIPFILE', + }, + }, + bucket: this.bucket!, + }; + } +} + +class AssetApplicationCode extends ApplicationCode { + private readonly path: string; + private readonly options?: s3_assets.AssetOptions; + private _asset?: s3_assets.Asset; + + constructor(path: string, options?: s3_assets.AssetOptions) { + super(); + this.path = path; + this.options = options; + } + + public bind(scope: Construct): ApplicationCodeConfig { + this._asset = new s3_assets.Asset(scope, 'Code', { + path: this.path, + ...this.options, + }); + + if (!this._asset.isZipArchive) { + throw new Error(`Asset must be a .zip file or a directory (${this.path})`); + } + + return { + applicationCodeConfigurationProperty: { + applicationCodeConfiguration: { + codeContent: { + s3ContentLocation: { + bucketArn: this._asset.bucket.bucketArn, + fileKey: this._asset.s3ObjectKey, + }, + }, + codeContentType: 'ZIPFILE', + }, + }, + bucket: this._asset.bucket, + }; + } + + get asset(): s3_assets.Asset | undefined { + return this._asset; + } + + get bucket(): s3.IBucket | undefined { + return this._asset?.bucket; + } +} diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/application.ts b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/application.ts new file mode 100644 index 0000000000000..1bcfa0fc4cbbf --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/application.ts @@ -0,0 +1,341 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as ka from '@aws-cdk/aws-kinesisanalytics'; +import * as logs from '@aws-cdk/aws-logs'; +import * as core from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { ApplicationCode } from './application-code'; +import { environmentProperties } from './private/environment-properties'; +import { flinkApplicationConfiguration } from './private/flink-application-configuration'; +import { validateFlinkApplicationProps as validateApplicationProps } from './private/validation'; +import { LogLevel, MetricsLevel, PropertyGroups, Runtime } from './types'; + +/** + * An interface expressing the public properties on both an imported and + * CDK-created Flink application. + */ +export interface IApplication extends core.IResource, iam.IGrantable { + /** + * The application ARN. + * + * @attribute + */ + readonly applicationArn: string; + + /** + * The name of the Flink application. + * + * @attribute + */ + readonly applicationName: string; + + /** + * The application IAM role. + */ + readonly role?: iam.IRole; + + /** + * Convenience method for adding a policy statement to the application role. + */ + addToRolePolicy(policyStatement: iam.PolicyStatement): boolean; +} + +/** + * Implements the functionality shared between CDK created and imported + * IApplications. + */ +abstract class ApplicationBase extends core.Resource implements IApplication { + public abstract readonly applicationArn: string; + public abstract readonly applicationName: string; + public abstract readonly role?: iam.IRole; + + // Implement iam.IGrantable interface + public abstract readonly grantPrincipal: iam.IPrincipal; + + /** Implement the convenience {@link IApplication.addToPrincipalPolicy} method. */ + public addToRolePolicy(policyStatement: iam.PolicyStatement): boolean { + if (this.role) { + this.role.addToPrincipalPolicy(policyStatement); + return true; + } + + return false; + } +} + +/** + * Props for creating an Application construct. + */ +export interface ApplicationProps { + /** + * A name for your Application that is unique to an AWS account. + * + * @default - CloudFormation-generated name + */ + readonly applicationName?: string; + + /** + * The Flink version to use for this application. + */ + readonly runtime: Runtime; + + /** + * The Flink code asset to run. + */ + readonly code: ApplicationCode; + + /** + * Whether checkpointing is enabled while your application runs. + * + * @default true + */ + readonly checkpointingEnabled?: boolean; + + /** + * The interval between checkpoints. + * + * @default 1 minute + */ + readonly checkpointInterval?: core.Duration; + + /** + * The minimum amount of time in to wait after a checkpoint finishes to start + * a new checkpoint. + * + * @default 5 seconds + */ + readonly minPauseBetweenCheckpoints?: core.Duration; + + /** + * The level of log verbosity from the Flink application. + * + * @default FlinkLogLevel.INFO + */ + readonly logLevel?: LogLevel; + + /** + * Describes the granularity of the CloudWatch metrics for an application. + * Use caution with Parallelism level metrics. Parallelism granularity logs + * metrics for each parallel thread and can quickly become expensive when + * parallelism is high (e.g. > 64). + * + * @default MetricsLevel.APPLICATION + */ + readonly metricsLevel?: MetricsLevel; + + /** + * Whether the Kinesis Data Analytics service can increase the parallelism of + * the application in response to resource usage. + * + * @default true + */ + readonly autoScalingEnabled?: boolean; + + /** + * The initial parallelism for the application. Kinesis Data Analytics can + * stop the app, increase the parallelism, and start the app again if + * autoScalingEnabled is true (the default value). + * + * @default 1 + */ + readonly parallelism?: number; + + /** + * The Flink parallelism allowed per Kinesis Processing Unit (KPU). + * + * @default 1 + */ + readonly parallelismPerKpu?: number + + /** + * Determines if Flink snapshots are enabled. + * + * @default true + */ + readonly snapshotsEnabled?: boolean; + + /** + * Configuration PropertyGroups. You can use these property groups to pass + * arbitrary runtime configuration values to your Flink app. + * + * @default No property group configuration provided to the Flink app + */ + readonly propertyGroups?: PropertyGroups; + + /** + * A role to use to grant permissions to your application. Prefer omitting + * this property and using the default role. + * + * @default - a new Role will be created + */ + readonly role?: iam.IRole; + + /** + * Provide a RemovalPolicy to override the default. + * + * @default RemovalPolicy.DESTROY + */ + readonly removalPolicy?: core.RemovalPolicy; + + /** + * The log group to send log entries to. + * + * @default CDK's default LogGroup + */ + readonly logGroup?: logs.ILogGroup; +} + +/** + * An imported Flink application. + */ +class Import extends ApplicationBase { + public readonly grantPrincipal: iam.IPrincipal; + public readonly role?: iam.IRole; + public readonly applicationName: string; + public readonly applicationArn: string; + + constructor(scope: Construct, id: string, attrs: { applicationArn: string, applicationName: string }) { + super(scope, id); + + // Imported applications have no associated role or grantPrincipal + this.grantPrincipal = new iam.UnknownPrincipal({ resource: this }); + this.role = undefined; + + this.applicationArn = attrs.applicationArn; + this.applicationName = attrs.applicationName; + } +} + +/** + * The L2 construct for Flink Kinesis Data Applications. + * + * @resource AWS::KinesisAnalyticsV2::Application + * + * @experimental + */ +export class Application extends ApplicationBase { + /** + * Import an existing Flink application defined outside of CDK code by + * applicationName. + */ + public static fromApplicationName(scope: Construct, id: string, applicationName: string): IApplication { + const applicationArn = core.Stack.of(scope).formatArn(applicationArnComponents(applicationName)); + + return new Import(scope, id, { applicationArn, applicationName }); + } + + /** + * Import an existing application defined outside of CDK code by + * applicationArn. + */ + public static fromApplicationArn(scope: Construct, id: string, applicationArn: string): IApplication { + const applicationName = core.Stack.of(scope).parseArn(applicationArn).resourceName; + if (!applicationName) { + throw new Error(`applicationArn for fromApplicationArn (${applicationArn}) must include resource name`); + } + + return new Import(scope, id, { applicationArn, applicationName }); + } + + public readonly applicationArn: string; + public readonly applicationName: string; + + // Role must be optional for JSII compatibility + public readonly role?: iam.IRole; + + public readonly grantPrincipal: iam.IPrincipal; + + constructor(scope: Construct, id: string, props: ApplicationProps) { + super(scope, id, { physicalName: props.applicationName }); + validateApplicationProps(props); + + this.role = props.role ?? new iam.Role(this, 'Role', { + assumedBy: new iam.ServicePrincipal('kinesisanalytics.amazonaws.com'), + }); + this.grantPrincipal = this.role; + + // Permit metric publishing to CloudWatch + this.role.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['cloudwatch:PutMetricData'], + resources: ['*'], + })); + + const code = props.code.bind(this); + code.bucket.grantRead(this); + + const resource = new ka.CfnApplicationV2(this, 'Resource', { + runtimeEnvironment: props.runtime.value, + serviceExecutionRole: this.role.roleArn, + applicationConfiguration: { + ...code.applicationCodeConfigurationProperty, + environmentProperties: environmentProperties(props.propertyGroups), + flinkApplicationConfiguration: flinkApplicationConfiguration({ + checkpointingEnabled: props.checkpointingEnabled, + checkpointInterval: props.checkpointInterval, + minPauseBetweenCheckpoints: props.minPauseBetweenCheckpoints, + logLevel: props.logLevel, + metricsLevel: props.metricsLevel, + autoScalingEnabled: props.autoScalingEnabled, + parallelism: props.parallelism, + parallelismPerKpu: props.parallelismPerKpu, + }), + applicationSnapshotConfiguration: { + snapshotsEnabled: props.snapshotsEnabled ?? true, + }, + }, + }); + resource.node.addDependency(this.role); + + const logGroup = props.logGroup ?? new logs.LogGroup(this, 'LogGroup'); + const logStream = new logs.LogStream(this, 'LogStream', { logGroup }); + + /* Permit logging */ + + this.role.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['logs:DescribeLogGroups'], + resources: [ + core.Stack.of(this).formatArn({ + service: 'logs', + resource: 'log-group', + sep: ':', + resourceName: '*', + }), + ], + })); + + this.role.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['logs:DescribeLogStreams'], + resources: [logGroup.logGroupArn], + })); + + const logStreamArn = `arn:${core.Aws.PARTITION}:logs:${core.Aws.REGION}:${core.Aws.ACCOUNT_ID}:log-group:${logGroup.logGroupName}:log-stream:${logStream.logStreamName}`; + this.role.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['logs:PutLogEvents'], + resources: [logStreamArn], + })); + + new ka.CfnApplicationCloudWatchLoggingOptionV2(this, 'LoggingOption', { + applicationName: resource.ref, + cloudWatchLoggingOption: { + logStreamArn, + }, + }); + + this.applicationName = this.getResourceNameAttribute(resource.ref); + this.applicationArn = this.getResourceArnAttribute( + core.Stack.of(this).formatArn(applicationArnComponents(resource.ref)), + applicationArnComponents(this.physicalName), + ); + + resource.applyRemovalPolicy(props.removalPolicy, { + default: core.RemovalPolicy.DESTROY, + }); + } +} + +function applicationArnComponents(resourceName: string): core.ArnComponents { + return { + service: 'kinesisanalytics', + resource: 'application', + resourceName, + }; +} diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/index.ts b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/index.ts new file mode 100644 index 0000000000000..6e2bea43a0ebf --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/index.ts @@ -0,0 +1,4 @@ +export * from './application'; +export * from './application-code'; +export * from './types'; + diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/private/environment-properties.ts b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/private/environment-properties.ts new file mode 100644 index 0000000000000..d13a0e870e23e --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/private/environment-properties.ts @@ -0,0 +1,16 @@ +import * as ka from '@aws-cdk/aws-kinesisanalytics'; +import { PropertyGroups } from '../types'; + +export function environmentProperties(propertyGroups?: PropertyGroups): ka.CfnApplicationV2.EnvironmentPropertiesProperty | undefined { + const entries = Object.entries(propertyGroups ?? {}); + if (entries.length === 0) { + return; + } + + return { + propertyGroups: entries.map(([id, map]) => ({ + propertyGroupId: id, + propertyMap: map, + })), + }; +} diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/private/flink-application-configuration.ts b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/private/flink-application-configuration.ts new file mode 100644 index 0000000000000..c0a93edac20d6 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/private/flink-application-configuration.ts @@ -0,0 +1,78 @@ +import * as core from '@aws-cdk/core'; +import { LogLevel, MetricsLevel } from '../types'; + +interface FlinkApplicationConfiguration extends + CheckpointConfiguration, + MonitoringConfiguration, + ParallelismConfiguration {} + +interface CheckpointConfiguration { + checkpointingEnabled?: boolean; + checkpointInterval?: core.Duration; + minPauseBetweenCheckpoints?: core.Duration; +} + +interface MonitoringConfiguration { + logLevel?: LogLevel; + metricsLevel?: MetricsLevel; +} + +interface ParallelismConfiguration { + autoScalingEnabled?: boolean; + parallelism?: number; + parallelismPerKpu?: number; +} + +/** + * Build the nested Cfn FlinkApplicationConfiguration object. This function + * doesn't return empty config objects, returning the minimal config needed to + * express the supplied properties. + * + * This function also handles the quirky configType: 'CUSTOM' setting required + * whenever config in one of the nested groupings. + */ +export function flinkApplicationConfiguration(config: FlinkApplicationConfiguration) { + const checkpointConfiguration = configFor({ + checkpointingEnabled: config.checkpointingEnabled, + checkpointInterval: config.checkpointInterval?.toMilliseconds(), + minPauseBetweenCheckpoints: config.minPauseBetweenCheckpoints?.toMilliseconds(), + }); + + const monitoringConfiguration = configFor({ + logLevel: config.logLevel, + metricsLevel: config.metricsLevel, + }); + + const parallelismConfiguration = configFor({ + autoScalingEnabled: config.autoScalingEnabled, + parallelism: config.parallelism, + parallelismPerKpu: config.parallelismPerKpu, + }); + + const applicationConfiguration = { + checkpointConfiguration, + monitoringConfiguration, + parallelismConfiguration, + }; + + if (isEmptyObj(applicationConfiguration)) { + return; + } + + return applicationConfiguration; +} + +function configFor(config: {[key: string]: unknown}) { + if (isEmptyObj(config)) { + return; + } + + return { + ...config, + configurationType: 'CUSTOM', + }; +} + +function isEmptyObj(obj: {[key: string]: unknown}) { + return Object.values(obj).every(v => v === undefined); +} diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/private/validation.ts b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/private/validation.ts new file mode 100644 index 0000000000000..b0f94f56daf77 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/private/validation.ts @@ -0,0 +1,54 @@ +import * as core from '@aws-cdk/core'; + +interface ValidatedProps { + applicationName?: string; + parallelism?: number; + parallelismPerKpu?: number; +} + +/** + * Early validation for the props used to create FlinkApplications. + */ +export function validateFlinkApplicationProps(props: ValidatedProps) { + validateApplicationName(props.applicationName); + validateParallelism(props.parallelism); + validateParallelismPerKpu(props.parallelismPerKpu); +} + +function validateApplicationName(applicationName?: string) { + if (applicationName === undefined || core.Token.isUnresolved(applicationName)) { + return; + } + + if (applicationName.length === 0) { + throw new Error('applicationName cannot be empty. It must contain at least 1 character.'); + } + + if (!/^[a-zA-Z0-9_.-]+$/.test(applicationName)) { + throw new Error(`applicationName may only contain letters, numbers, underscores, hyphens, and periods. Name: ${applicationName}`); + } + + if (applicationName.length > 128) { + throw new Error(`applicationName max length is 128. Name: ${applicationName} is ${applicationName.length} characters.`); + } +} + +function validateParallelism(parallelism?: number) { + if (parallelism === undefined || core.Token.isUnresolved(parallelism)) { + return; + } + + if (parallelism < 1) { + throw new Error('parallelism must be at least 1'); + } +} + +function validateParallelismPerKpu(parallelismPerKpu?: number) { + if (parallelismPerKpu === undefined || core.Token.isUnresolved(parallelismPerKpu)) { + return; + } + + if (parallelismPerKpu < 1) { + throw new Error('parallelismPerKpu must be at least 1'); + } +} diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/types.ts b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/types.ts new file mode 100644 index 0000000000000..b92d55cb48518 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/lib/types.ts @@ -0,0 +1,67 @@ +/** + * Available log levels for Flink applications. + */ +export enum LogLevel { + /** Debug level logging */ + DEBUG = 'DEBUG', + + /** Info level logging */ + INFO = 'INFO', + + /** Warn level logging */ + WARN = 'WARN', + + /** Error level logging */ + ERROR = 'ERROR', +} + +/** + * Granularity of metrics sent to CloudWatch. + */ +export enum MetricsLevel { + /** Application sends the least metrics to CloudWatch */ + APPLICATION = 'APPLICATION', + + /** Task includes task-level metrics sent to CloudWatch */ + TASK = 'TASK', + + /** Operator includes task-level and operator-level metrics sent to CloudWatch */ + OPERATOR = 'OPERATOR', + + /** Send all metrics including metrics per task thread */ + PARALLELISM = 'PARALLELISM', +} + +/** + * Interface for building AWS::KinesisAnalyticsV2::Application PropertyGroup + * configuration. + */ +export interface PropertyGroups { + readonly [propertyId: string]: {[mapKey: string]: string}; +} + +/** + * Available Flink runtimes for Kinesis Analytics. + */ +export class Runtime { + /** Flink Version 1.6 */ + public static readonly FLINK_1_6 = Runtime.of('FLINK-1_6'); + + /** Flink Version 1.8 */ + public static readonly FLINK_1_8 = Runtime.of('FLINK-1_8'); + + /** Flink Version 1.11 */ + public static readonly FLINK_1_11 = Runtime.of('FLINK-1_11'); + + /** Create a new Runtime with with an arbitrary Flink version string */ + public static of(value: string) { + return new Runtime(value); + } + + /** The Cfn string that represents a version of Flink */ + public readonly value: string; + + private constructor(value: string) { + this.value = value; + } +} diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json b/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json new file mode 100644 index 0000000000000..80dbdfd4aeb76 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/package.json @@ -0,0 +1,115 @@ +{ + "name": "@aws-cdk/aws-kinesisanalytics-flink", + "version": "0.0.0", + "description": "A CDK Construct Library for Kinesis Analytics Flink applications", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.services.kinesis.analytics.flink", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "kinesisanalytics-flink" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.AWS.KinesisAnalyticsFlink", + "packageId": "Amazon.CDK.AWS.KinesisAnalyticsFlink", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-cdk.aws-kinesisanalytics-flink", + "module": "aws_cdk.aws_kinesisanalytics_flink", + "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-kinesisanalytics-flink" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "compat": "cdk-compat", + "rosetta:extract": "yarn --silent jsii-rosetta extract" + }, + "keywords": [ + "aws", + "cdk", + "kinesis", + "analytics", + "kinesisanalytcs", + "flink" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", + "jest": "^26.6.3", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/assets": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kinesisanalytics": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", + "constructs": "^3.2.0" + }, + "homepage": "https://github.com/aws/aws-cdk", + "peerDependencies": { + "@aws-cdk/assets": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kinesisanalytics": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", + "constructs": "^3.2.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "awslint": { + "exclude": [ + "props-physical-name:@aws-cdk/aws-kinesisanalytics-flink.ApplicationProps" + ] + }, + "stability": "experimental", + "maturity": "experimental", + "awscdkio": { + "announce": false + }, + "cdk-build": { + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": true + } + } +} diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/test/application.test.ts b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/application.test.ts new file mode 100644 index 0000000000000..6dc35fadd44b6 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/application.test.ts @@ -0,0 +1,604 @@ +import { arrayWith, objectLike, ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import * as logs from '@aws-cdk/aws-logs'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as core from '@aws-cdk/core'; +import * as path from 'path'; +import * as flink from '../lib'; + +describe('Application', () => { + let stack: core.Stack; + let bucket: s3.Bucket; + let requiredProps: { + runtime: flink.Runtime; + code: flink.ApplicationCode; + }; + + beforeEach(() => { + stack = new core.Stack(); + bucket = new s3.Bucket(stack, 'CodeBucket'); + requiredProps = { + runtime: flink.Runtime.FLINK_1_11, + code: flink.ApplicationCode.fromBucket(bucket, 'my-app.jar'), + }; + }); + + test('default Flink Application', () => { + new flink.Application(stack, 'FlinkApplication', { + runtime: flink.Runtime.FLINK_1_11, + code: flink.ApplicationCode.fromBucket(bucket, 'my-app.jar'), + }); + + expect(stack).toHaveResource('AWS::KinesisAnalyticsV2::Application', { + RuntimeEnvironment: 'FLINK-1_11', + ServiceExecutionRole: { + 'Fn::GetAtt': [ + 'FlinkApplicationRole2F7BCBF6', + 'Arn', + ], + }, + ApplicationConfiguration: { + ApplicationCodeConfiguration: { + CodeContent: { + S3ContentLocation: { + BucketARN: stack.resolve(bucket.bucketArn), + FileKey: 'my-app.jar', + }, + }, + CodeContentType: 'ZIPFILE', + }, + ApplicationSnapshotConfiguration: { + SnapshotsEnabled: true, + }, + }, + }); + + expect(stack).toHaveResourceLike('AWS::KinesisAnalyticsV2::Application', { + DeletionPolicy: 'Delete', + }, ResourcePart.CompleteDefinition); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'kinesisanalytics.amazonaws.com', + }, + }], + Version: '2012-10-17', + }, + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith( + { Action: 'cloudwatch:PutMetricData', Effect: 'Allow', Resource: '*' }, + { + Action: 'logs:DescribeLogGroups', + Effect: 'Allow', + Resource: { + // looks like arn:aws:logs:us-east-1:123456789012:log-group:*, + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':logs:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':log-group:*', + ]], + }, + }, + { + Action: 'logs:DescribeLogStreams', + Effect: 'Allow', + Resource: { + // looks like: arn:aws:logs:us-east-1:123456789012:log-group:my-log-group:*, + 'Fn::GetAtt': ['FlinkApplicationLogGroup7739479C', 'Arn'], + }, + }, + { + Action: 'logs:PutLogEvents', + Effect: 'Allow', + Resource: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':logs:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':log-group:', + { Ref: 'FlinkApplicationLogGroup7739479C' }, + ':log-stream:', + { Ref: 'FlinkApplicationLogStreamB633AF32' }, + ]], + }, + }, + ), + }, + }); + }); + + test('providing a custom role', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + role: new iam.Role(stack, 'CustomRole', { + assumedBy: new iam.ServicePrincipal('custom-principal'), + }), + }); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'custom-principal.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); + }); + + test('addToPrincipalPolicy', () => { + const app = new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + }); + + app.addToRolePolicy(new iam.PolicyStatement({ + actions: ['custom:action'], + resources: ['*'], + })); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith( + objectLike({ Action: 'custom:action', Effect: 'Allow', Resource: '*' }), + ), + }, + }); + }); + + test('providing a custom runtime', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + runtime: flink.Runtime.of('custom'), + }); + + expect(stack).toHaveResourceLike('AWS::KinesisAnalyticsV2::Application', { + RuntimeEnvironment: 'custom', + }); + }); + + test('providing a custom removal policy', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + removalPolicy: core.RemovalPolicy.RETAIN, + }); + + expect(stack).toHaveResourceLike('AWS::KinesisAnalyticsV2::Application', { + DeletionPolicy: 'Retain', + }, ResourcePart.CompleteDefinition); + }); + + test('granting permissions to resources', () => { + const app = new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + }); + + const dataBucket = new s3.Bucket(stack, 'DataBucket'); + dataBucket.grantRead(app); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: arrayWith( + objectLike({ Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'] }), + ), + }, + }); + }); + + test('using an asset for code', () => { + const code = flink.ApplicationCode.fromAsset(path.join(__dirname, 'code-asset')); + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + code, + }); + const assetRef = 'AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3BucketEBA17A67'; + const versionKeyRef = 'AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3VersionKey5922697E'; + + expect(stack).toHaveResourceLike('AWS::KinesisAnalyticsV2::Application', { + ApplicationConfiguration: { + ApplicationCodeConfiguration: { + CodeContent: { + S3ContentLocation: { + BucketARN: { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':s3:::', + { Ref: assetRef }, + ]], + }, + FileKey: { + 'Fn::Join': ['', [ + { 'Fn::Select': [0, { 'Fn::Split': ['||', { Ref: versionKeyRef }] }] }, + { 'Fn::Select': [1, { 'Fn::Split': ['||', { Ref: versionKeyRef }] }] }, + ]], + }, + }, + }, + CodeContentType: 'ZIPFILE', + }, + }, + }); + }); + + test('adding property groups', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + propertyGroups: { + FlinkApplicationProperties: { + SomeProperty: 'SomeValue', + }, + }, + }); + + expect(stack).toHaveResourceLike('AWS::KinesisAnalyticsV2::Application', { + ApplicationConfiguration: { + EnvironmentProperties: { + PropertyGroups: [ + { + PropertyGroupId: 'FlinkApplicationProperties', + PropertyMap: { + SomeProperty: 'SomeValue', + }, + }, + ], + }, + }, + }); + }); + + test('checkpointEnabled setting', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + checkpointingEnabled: false, + }); + + expect(stack).toHaveResourceLike('AWS::KinesisAnalyticsV2::Application', { + ApplicationConfiguration: { + FlinkApplicationConfiguration: { + CheckpointConfiguration: { + ConfigurationType: 'CUSTOM', + CheckpointingEnabled: false, + }, + }, + }, + }); + }); + + test('checkpointInterval setting', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + checkpointInterval: core.Duration.minutes(5), + }); + + expect(stack).toHaveResourceLike('AWS::KinesisAnalyticsV2::Application', { + ApplicationConfiguration: { + FlinkApplicationConfiguration: { + CheckpointConfiguration: { + ConfigurationType: 'CUSTOM', + CheckpointInterval: 300_000, + }, + }, + }, + }); + }); + + test('minPauseBetweenCheckpoints setting', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + minPauseBetweenCheckpoints: core.Duration.seconds(10), + }); + + expect(stack).toHaveResourceLike('AWS::KinesisAnalyticsV2::Application', { + ApplicationConfiguration: { + FlinkApplicationConfiguration: { + CheckpointConfiguration: { + ConfigurationType: 'CUSTOM', + MinPauseBetweenCheckpoints: 10_000, + }, + }, + }, + }); + }); + + test('logLevel setting', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + logLevel: flink.LogLevel.DEBUG, + }); + + expect(stack).toHaveResourceLike('AWS::KinesisAnalyticsV2::Application', { + ApplicationConfiguration: { + FlinkApplicationConfiguration: { + MonitoringConfiguration: { + ConfigurationType: 'CUSTOM', + LogLevel: 'DEBUG', + }, + }, + }, + }); + }); + + test('metricsLevel setting', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + metricsLevel: flink.MetricsLevel.PARALLELISM, + }); + + expect(stack).toHaveResourceLike('AWS::KinesisAnalyticsV2::Application', { + ApplicationConfiguration: { + FlinkApplicationConfiguration: { + MonitoringConfiguration: { + ConfigurationType: 'CUSTOM', + MetricsLevel: 'PARALLELISM', + }, + }, + }, + }); + }); + + test('autoscalingEnabled setting', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + autoScalingEnabled: false, + }); + + expect(stack).toHaveResourceLike('AWS::KinesisAnalyticsV2::Application', { + ApplicationConfiguration: { + FlinkApplicationConfiguration: { + ParallelismConfiguration: { + ConfigurationType: 'CUSTOM', + AutoScalingEnabled: false, + }, + }, + }, + }); + }); + + test('parallelism setting', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + parallelism: 2, + }); + + expect(stack).toHaveResourceLike('AWS::KinesisAnalyticsV2::Application', { + ApplicationConfiguration: { + FlinkApplicationConfiguration: { + ParallelismConfiguration: { + ConfigurationType: 'CUSTOM', + Parallelism: 2, + }, + }, + }, + }); + }); + + test('parallelismPerKpu setting', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + parallelismPerKpu: 2, + }); + + expect(stack).toHaveResourceLike('AWS::KinesisAnalyticsV2::Application', { + ApplicationConfiguration: { + FlinkApplicationConfiguration: { + ParallelismConfiguration: { + ConfigurationType: 'CUSTOM', + ParallelismPerKPU: 2, + }, + }, + }, + }); + }); + + test('snapshotsEnabled setting', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + snapshotsEnabled: false, + }); + + expect(stack).toHaveResourceLike('AWS::KinesisAnalyticsV2::Application', { + ApplicationConfiguration: { + ApplicationSnapshotConfiguration: { + SnapshotsEnabled: false, + }, + }, + }); + }); + + test('default logging option', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + snapshotsEnabled: false, + }); + + expect(stack).toHaveResource('AWS::KinesisAnalyticsV2::ApplicationCloudWatchLoggingOption', { + ApplicationName: { + Ref: 'FlinkApplicationC5836815', + }, + CloudWatchLoggingOption: { + LogStreamARN: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':log-group:', + { + Ref: 'FlinkApplicationLogGroup7739479C', + }, + ':log-stream:', + { + Ref: 'FlinkApplicationLogStreamB633AF32', + }, + ], + ], + }, + }, + }); + + expect(stack).toHaveResource('AWS::Logs::LogGroup', { + Properties: { + RetentionInDays: 731, + }, + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + }, ResourcePart.CompleteDefinition); + + expect(stack).toHaveResource('AWS::Logs::LogStream', { + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + }, ResourcePart.CompleteDefinition); + }); + + test('logGroup setting', () => { + new flink.Application(stack, 'FlinkApplication', { + ...requiredProps, + logGroup: new logs.LogGroup(stack, 'LogGroup', { + logGroupName: 'custom', + }), + }); + + expect(stack).toHaveResource('AWS::Logs::LogGroup', { + LogGroupName: 'custom', + }); + }); + + test('validating applicationName', () => { + // Expect no error with valid name + new flink.Application(stack, 'ValidString', { + ...requiredProps, + applicationName: 'my-VALID.app_name', + }); + + // Expect no error with ref + new flink.Application(stack, 'ValidRef', { + ...requiredProps, + applicationName: new core.CfnParameter(stack, 'Parameter').valueAsString, + }); + + expect(() => { + new flink.Application(stack, 'Empty', { + ...requiredProps, + applicationName: '', + }); + }).toThrow(/cannot be empty/); + + expect(() => { + new flink.Application(stack, 'InvalidCharacters', { + ...requiredProps, + applicationName: '!!!', + }); + }).toThrow(/may only contain letters, numbers, underscores, hyphens, and periods/); + + expect(() => { + new flink.Application(stack, 'TooLong', { + ...requiredProps, + applicationName: 'a'.repeat(129), + }); + }).toThrow(/max length is 128/); + }); + + test('validating parallelism', () => { + // Expect no error with valid value + new flink.Application(stack, 'ValidNumber', { + ...requiredProps, + parallelism: 32, + }); + + // Expect no error with ref + new flink.Application(stack, 'ValidRef', { + ...requiredProps, + parallelism: new core.CfnParameter(stack, 'Parameter', { + type: 'Number', + }).valueAsNumber, + }); + + expect(() => { + new flink.Application(stack, 'TooSmall', { + ...requiredProps, + parallelism: 0, + }); + }).toThrow(/must be at least 1/); + }); + + test('validating parallelismPerKpu', () => { + // Expect no error with valid value + new flink.Application(stack, 'ValidNumber', { + ...requiredProps, + parallelismPerKpu: 10, + }); + + // Expect no error with ref + new flink.Application(stack, 'ValidRef', { + ...requiredProps, + parallelismPerKpu: new core.CfnParameter(stack, 'Parameter', { + type: 'Number', + }).valueAsNumber, + }); + + expect(() => { + new flink.Application(stack, 'TooSmall', { + ...requiredProps, + parallelismPerKpu: 0, + }); + }).toThrow(/must be at least 1/); + }); + + test('fromFlinkApplicationName', () => { + const flinkApp = flink.Application.fromApplicationName(stack, 'Imported', 'my-app'); + + expect(flinkApp.applicationName).toEqual('my-app'); + expect(stack.resolve(flinkApp.applicationArn)).toEqual({ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':kinesisanalytics:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':application/my-app', + ]], + }); + expect(flinkApp.addToRolePolicy(new iam.PolicyStatement())).toBe(false); + }); + + test('fromFlinkApplicationArn', () => { + const arn = 'arn:aws:kinesisanalytics:us-west-2:012345678901:application/my-app'; + const flinkApp = flink.Application.fromApplicationArn(stack, 'Imported', arn); + + expect(flinkApp.applicationName).toEqual('my-app'); + expect(flinkApp.applicationArn).toEqual(arn); + expect(flinkApp.addToRolePolicy(new iam.PolicyStatement())).toBe(false); + }); +}); diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/test/code-asset/WordCount.jar b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/code-asset/WordCount.jar new file mode 100644 index 0000000000000..9c533e6fea607 Binary files /dev/null and b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/code-asset/WordCount.jar differ diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.application-code-from-bucket.lit.expected.json b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.application-code-from-bucket.lit.expected.json new file mode 100644 index 0000000000000..6ab6a5f40bcab --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.application-code-from-bucket.lit.expected.json @@ -0,0 +1,295 @@ +{ + "Parameters": { + "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3BucketEBA17A67": { + "Type": "String", + "Description": "S3 bucket for asset \"8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577\"" + }, + "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3VersionKey5922697E": { + "Type": "String", + "Description": "S3 key for asset version \"8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577\"" + }, + "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577ArtifactHash211A4F2F": { + "Type": "String", + "Description": "Artifact hash for asset \"8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577\"" + } + }, + "Resources": { + "AppRole1AF9B530": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "kinesisanalytics.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "AppRoleDefaultPolicy9CADBAA1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "cloudwatch:PutMetricData", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3BucketEBA17A67" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3BucketEBA17A67" + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "logs:DescribeLogGroups", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:*" + ] + ] + } + }, + { + "Action": "logs:DescribeLogStreams", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "AppLogGroupC72EEC8C", + "Arn" + ] + } + }, + { + "Action": "logs:PutLogEvents", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "AppLogGroupC72EEC8C" + }, + ":log-stream:", + { + "Ref": "AppLogStream3CAF66A7" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AppRoleDefaultPolicy9CADBAA1", + "Roles": [ + { + "Ref": "AppRole1AF9B530" + } + ] + } + }, + "AppF1B96344": { + "Type": "AWS::KinesisAnalyticsV2::Application", + "Properties": { + "RuntimeEnvironment": "FLINK-1_11", + "ServiceExecutionRole": { + "Fn::GetAtt": [ + "AppRole1AF9B530", + "Arn" + ] + }, + "ApplicationConfiguration": { + "ApplicationCodeConfiguration": { + "CodeContent": { + "S3ContentLocation": { + "BucketARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3BucketEBA17A67" + } + ] + ] + }, + "FileKey": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3VersionKey5922697E" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3VersionKey5922697E" + } + ] + } + ] + } + ] + ] + } + } + }, + "CodeContentType": "ZIPFILE" + }, + "ApplicationSnapshotConfiguration": { + "SnapshotsEnabled": true + } + } + }, + "DependsOn": [ + "AppRoleDefaultPolicy9CADBAA1", + "AppRole1AF9B530" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AppLogGroupC72EEC8C": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "AppLogStream3CAF66A7": { + "Type": "AWS::Logs::LogStream", + "Properties": { + "LogGroupName": { + "Ref": "AppLogGroupC72EEC8C" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "AppLoggingOption75BE995E": { + "Type": "AWS::KinesisAnalyticsV2::ApplicationCloudWatchLoggingOption", + "Properties": { + "ApplicationName": { + "Ref": "AppF1B96344" + }, + "CloudWatchLoggingOption": { + "LogStreamARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "AppLogGroupC72EEC8C" + }, + ":log-stream:", + { + "Ref": "AppLogStream3CAF66A7" + } + ] + ] + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.application-code-from-bucket.lit.ts b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.application-code-from-bucket.lit.ts new file mode 100644 index 0000000000000..eb8c5ce3e9d03 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.application-code-from-bucket.lit.ts @@ -0,0 +1,22 @@ +import * as path from 'path'; +import * as assets from '@aws-cdk/aws-s3-assets'; +import * as core from '@aws-cdk/core'; +import * as flink from '../lib'; + +const app = new core.App(); +const stack = new core.Stack(app, 'FlinkAppCodeFromBucketTest'); + +const asset = new assets.Asset(stack, 'CodeAsset', { + path: path.join(__dirname, 'code-asset'), +}); +const bucket = asset.bucket; +const fileKey = asset.s3ObjectKey; + +///! show +new flink.Application(stack, 'App', { + code: flink.ApplicationCode.fromBucket(bucket, fileKey), + runtime: flink.Runtime.FLINK_1_11, +}); +///! hide + +app.synth(); diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.application.lit.expected.json b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.application.lit.expected.json new file mode 100644 index 0000000000000..3b4f7ecf64f7e --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.application.lit.expected.json @@ -0,0 +1,295 @@ +{ + "Resources": { + "AppRole1AF9B530": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "kinesisanalytics.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "AppRoleDefaultPolicy9CADBAA1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "cloudwatch:PutMetricData", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3BucketEBA17A67" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3BucketEBA17A67" + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "logs:DescribeLogGroups", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:*" + ] + ] + } + }, + { + "Action": "logs:DescribeLogStreams", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "AppLogGroupC72EEC8C", + "Arn" + ] + } + }, + { + "Action": "logs:PutLogEvents", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "AppLogGroupC72EEC8C" + }, + ":log-stream:", + { + "Ref": "AppLogStream3CAF66A7" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AppRoleDefaultPolicy9CADBAA1", + "Roles": [ + { + "Ref": "AppRole1AF9B530" + } + ] + } + }, + "AppF1B96344": { + "Type": "AWS::KinesisAnalyticsV2::Application", + "Properties": { + "RuntimeEnvironment": "FLINK-1_11", + "ServiceExecutionRole": { + "Fn::GetAtt": [ + "AppRole1AF9B530", + "Arn" + ] + }, + "ApplicationConfiguration": { + "ApplicationCodeConfiguration": { + "CodeContent": { + "S3ContentLocation": { + "BucketARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3BucketEBA17A67" + } + ] + ] + }, + "FileKey": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3VersionKey5922697E" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3VersionKey5922697E" + } + ] + } + ] + } + ] + ] + } + } + }, + "CodeContentType": "ZIPFILE" + }, + "ApplicationSnapshotConfiguration": { + "SnapshotsEnabled": true + } + } + }, + "DependsOn": [ + "AppRoleDefaultPolicy9CADBAA1", + "AppRole1AF9B530" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AppLogGroupC72EEC8C": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "AppLogStream3CAF66A7": { + "Type": "AWS::Logs::LogStream", + "Properties": { + "LogGroupName": { + "Ref": "AppLogGroupC72EEC8C" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "AppLoggingOption75BE995E": { + "Type": "AWS::KinesisAnalyticsV2::ApplicationCloudWatchLoggingOption", + "Properties": { + "ApplicationName": { + "Ref": "AppF1B96344" + }, + "CloudWatchLoggingOption": { + "LogStreamARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:", + { + "Ref": "AppLogGroupC72EEC8C" + }, + ":log-stream:", + { + "Ref": "AppLogStream3CAF66A7" + } + ] + ] + } + } + } + } + }, + "Parameters": { + "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3BucketEBA17A67": { + "Type": "String", + "Description": "S3 bucket for asset \"8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577\"" + }, + "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577S3VersionKey5922697E": { + "Type": "String", + "Description": "S3 key for asset version \"8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577\"" + }, + "AssetParameters8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577ArtifactHash211A4F2F": { + "Type": "String", + "Description": "Artifact hash for asset \"8be9e0b5f53d41e9a3b1d51c9572c65f24f8170a7188d0ed57fb7d571de4d577\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.application.lit.ts b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.application.lit.ts new file mode 100644 index 0000000000000..02a6a9949dcfa --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics-flink/test/integ.application.lit.ts @@ -0,0 +1,15 @@ +///! show +import * as path from 'path'; +import * as core from '@aws-cdk/core'; +import * as flink from '../lib'; + +const app = new core.App(); +const stack = new core.Stack(app, 'FlinkAppTest'); + +new flink.Application(stack, 'App', { + code: flink.ApplicationCode.fromAsset(path.join(__dirname, 'code-asset')), + runtime: flink.Runtime.FLINK_1_11, +}); +///! hide + +app.synth(); diff --git a/packages/@aws-cdk/aws-kinesisanalytics/README.md b/packages/@aws-cdk/aws-kinesisanalytics/README.md index e65b812cefc97..33dfe482719b0 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics/README.md +++ b/packages/@aws-cdk/aws-kinesisanalytics/README.md @@ -14,3 +14,10 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +## Kinesis Analytics Flink + +The `aws-kinesisanalytics-flink` package provides constructs for building Flink applications. + + * [Github](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-kinesisanalytics-flink) + * [CDK Docs](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-kinesisanalytics-flink.html) diff --git a/packages/@aws-cdk/aws-kms/lib/key.ts b/packages/@aws-cdk/aws-kms/lib/key.ts index d8de804e61a12..4e18d228fe38e 100644 --- a/packages/@aws-cdk/aws-kms/lib/key.ts +++ b/packages/@aws-cdk/aws-kms/lib/key.ts @@ -156,7 +156,7 @@ abstract class KeyBase extends Resource implements IKey { resourceArns: [this.keyArn], resourceSelfArns: crossEnvironment ? undefined : ['*'], }; - if (this.trustAccountIdentities) { + if (this.trustAccountIdentities && !crossEnvironment) { return iam.Grant.addToPrincipalOrResource(grantOptions); } else { return iam.Grant.addToPrincipalAndResource({ diff --git a/packages/@aws-cdk/aws-kms/test/alias.test.ts b/packages/@aws-cdk/aws-kms/test/alias.test.ts index b39ea8bf5b232..6c4356a7e8f36 100644 --- a/packages/@aws-cdk/aws-kms/test/alias.test.ts +++ b/packages/@aws-cdk/aws-kms/test/alias.test.ts @@ -1,9 +1,13 @@ import '@aws-cdk/assert/jest'; import { ArnPrincipal, PolicyStatement } from '@aws-cdk/aws-iam'; -import { App, CfnOutput, Construct, Stack } from '@aws-cdk/core'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; import { Alias } from '../lib/alias'; import { IKey, Key } from '../lib/key'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + test('default alias', () => { const app = new App(); const stack = new Stack(app, 'Test'); diff --git a/packages/@aws-cdk/aws-kms/test/key.test.ts b/packages/@aws-cdk/aws-kms/test/key.test.ts index 1bc64d63a13d4..b2d37496d00ec 100644 --- a/packages/@aws-cdk/aws-kms/test/key.test.ts +++ b/packages/@aws-cdk/aws-kms/test/key.test.ts @@ -2,6 +2,7 @@ import { arrayWith, ResourcePart } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; +import { testFutureBehavior, testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as kms from '../lib'; const ADMIN_ACTIONS: string[] = [ @@ -39,19 +40,10 @@ const LEGACY_ADMIN_ACTIONS: string[] = [ 'kms:UntagResource', ]; -let app: cdk.App; -let stack: cdk.Stack; -beforeEach(() => { - app = new cdk.App({ - context: { - // By default, enable the correct key policy behavior. Specific tests will test the disabled behavior. - '@aws-cdk/aws-kms:defaultKeyPolicies': true, - }, - }); - stack = new cdk.Stack(app); -}); +const flags = { '@aws-cdk/aws-kms:defaultKeyPolicies': true }; -test('default key', () => { +testFutureBehavior('default key', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); new kms.Key(stack, 'MyKey'); expect(stack).toHaveResource('AWS::KMS::Key', { @@ -75,14 +67,16 @@ test('default key', () => { }, ResourcePart.CompleteDefinition); }); -test('default with no retention', () => { +testFutureBehavior('default with no retention', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); new kms.Key(stack, 'MyKey', { removalPolicy: cdk.RemovalPolicy.DESTROY }); expect(stack).toHaveResource('AWS::KMS::Key', { DeletionPolicy: 'Delete', UpdateReplacePolicy: 'Delete' }, ResourcePart.CompleteDefinition); }); describe('key policies', () => { - test('can specify a default key policy', () => { + testFutureBehavior('can specify a default key policy', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); const policy = new iam.PolicyDocument(); const statement = new iam.PolicyStatement({ resources: ['*'], actions: ['kms:Put*'] }); statement.addArnPrincipal('arn:aws:iam::111122223333:root'); @@ -107,7 +101,8 @@ describe('key policies', () => { }); }); - test('can append to the default key policy', () => { + testFutureBehavior('can append to the default key policy', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); const statement = new iam.PolicyStatement({ resources: ['*'], actions: ['kms:Put*'] }); statement.addArnPrincipal('arn:aws:iam::111122223333:root'); @@ -139,16 +134,53 @@ describe('key policies', () => { }); }); - test.each([ - ['decrypt', (key: kms.Key, user: iam.IGrantable) => key.grantDecrypt(user), 'kms:Decrypt'], - ['encrypt', (key: kms.Key, user: iam.IGrantable) => key.grantEncrypt(user), ['kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*']], - ])('grant %s', (_, grantFn, actions) => { + testFutureBehavior('decrypt', flags, cdk.App, (app) => { + // GIVEN + const stack = new cdk.Stack(app); + const key = new kms.Key(stack, 'Key'); + const user = new iam.User(stack, 'User'); + + // WHEN + key.grantDecrypt(user); + + // THEN + // Key policy should be unmodified by the grant. + expect(stack).toHaveResource('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + { + Action: 'kms:*', + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':root']] } }, + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'kms:Decrypt', + Effect: 'Allow', + Resource: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, + }, + ], + Version: '2012-10-17', + }, + }); + }); + + testFutureBehavior('encrypt', flags, cdk.App, (app) => { // GIVEN + const stack = new cdk.Stack(app); const key = new kms.Key(stack, 'Key'); const user = new iam.User(stack, 'User'); // WHEN - grantFn(key, user); + key.grantEncrypt(user); // THEN // Key policy should be unmodified by the grant. @@ -170,7 +202,7 @@ describe('key policies', () => { PolicyDocument: { Statement: [ { - Action: actions, + Action: ['kms:Encrypt', 'kms:ReEncrypt*', 'kms:GenerateDataKey*'], Effect: 'Allow', Resource: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, }, @@ -180,7 +212,7 @@ describe('key policies', () => { }); }); - test('grant for a principal in a dependent stack works correctly', () => { + testFutureBehavior('grant for a principal in a dependent stack works correctly', flags, cdk.App, (app) => { const principalStack = new cdk.Stack(app, 'PrincipalStack'); const principal = new iam.Role(principalStack, 'Role', { assumedBy: new iam.AnyPrincipal(), @@ -213,7 +245,105 @@ describe('key policies', () => { }); }); - test('additional key admins can be specified (with imported/immutable principal)', () => { + testFutureBehavior('grant for a principal in a different region', flags, cdk.App, (app) => { + const principalStack = new cdk.Stack(app, 'PrincipalStack', { env: { region: 'testregion1' } }); + const principal = new iam.Role(principalStack, 'Role', { + assumedBy: new iam.AnyPrincipal(), + roleName: 'MyRolePhysicalName', + }); + + const keyStack = new cdk.Stack(app, 'KeyStack', { env: { region: 'testregion2' } }); + const key = new kms.Key(keyStack, 'Key'); + + key.grantEncrypt(principal); + + expect(keyStack).toHaveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: arrayWith( + { + Action: [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':role/MyRolePhysicalName']] } }, + Resource: '*', + }, + ), + Version: '2012-10-17', + }, + }); + expect(principalStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }); + }); + + testFutureBehavior('grant for a principal in a different account', flags, cdk.App, (app) => { + const principalStack = new cdk.Stack(app, 'PrincipalStack', { env: { account: '0123456789012' } }); + const principal = new iam.Role(principalStack, 'Role', { + assumedBy: new iam.AnyPrincipal(), + roleName: 'MyRolePhysicalName', + }); + + const keyStack = new cdk.Stack(app, 'KeyStack', { env: { account: '111111111111' } }); + const key = new kms.Key(keyStack, 'Key'); + + key.grantEncrypt(principal); + + expect(keyStack).toHaveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + { + // Default policy, unmodified + }, + { + Action: [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::0123456789012:role/MyRolePhysicalName']] } }, + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }); + expect(principalStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }); + }); + + testFutureBehavior('additional key admins can be specified (with imported/immutable principal)', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); const adminRole = iam.Role.fromRoleArn(stack, 'Admin', 'arn:aws:iam::123456789012:role/TrustedAdmin'); new kms.Key(stack, 'MyKey', { admins: [adminRole] }); @@ -242,7 +372,8 @@ describe('key policies', () => { }); }); - test('additional key admins can be specified (with owned/mutable principal)', () => { + testFutureBehavior('additional key admins can be specified (with owned/mutable principal)', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); const adminRole = new iam.Role(stack, 'AdminRole', { assumedBy: new iam.AccountRootPrincipal(), }); @@ -279,7 +410,8 @@ describe('key policies', () => { }); }); -test('key with some options', () => { +testFutureBehavior('key with some options', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); const key = new kms.Key(stack, 'MyKey', { enableKeyRotation: true, enabled: false, @@ -311,17 +443,20 @@ test('key with some options', () => { }); }); -test('setting pendingWindow value to not in allowed range will throw', () => { +testFutureBehavior('setting pendingWindow value to not in allowed range will throw', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); expect(() => new kms.Key(stack, 'MyKey', { enableKeyRotation: true, pendingWindow: cdk.Duration.days(6) })) .toThrow('\'pendingWindow\' value must between 7 and 30 days. Received: 6'); }); -test('setting trustAccountIdentities to false will throw (when the defaultKeyPolicies feature flag is enabled)', () => { +testFutureBehavior('setting trustAccountIdentities to false will throw (when the defaultKeyPolicies feature flag is enabled)', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); expect(() => new kms.Key(stack, 'MyKey', { trustAccountIdentities: false })) .toThrow('`trustAccountIdentities` cannot be false if the @aws-cdk/aws-kms:defaultKeyPolicies feature flag is set'); }); -test('addAlias creates an alias', () => { +testFutureBehavior('addAlias creates an alias', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); const key = new kms.Key(stack, 'MyKey', { enableKeyRotation: true, enabled: false, @@ -342,7 +477,8 @@ test('addAlias creates an alias', () => { }); }); -test('can run multiple addAlias', () => { +testFutureBehavior('can run multiple addAlias', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); const key = new kms.Key(stack, 'MyKey', { enableKeyRotation: true, enabled: false, @@ -374,7 +510,8 @@ test('can run multiple addAlias', () => { }); }); -test('keyId resolves to a Ref', () => { +testFutureBehavior('keyId resolves to a Ref', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); const key = new kms.Key(stack, 'MyKey'); new cdk.CfnOutput(stack, 'Out', { @@ -387,7 +524,8 @@ test('keyId resolves to a Ref', () => { }); }); -test('fails if key policy has no actions', () => { +testFutureBehavior('fails if key policy has no actions', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); const key = new kms.Key(stack, 'MyKey'); key.addToResourcePolicy(new iam.PolicyStatement({ @@ -398,7 +536,8 @@ test('fails if key policy has no actions', () => { expect(() => app.synth()).toThrow(/A PolicyStatement must specify at least one \'action\' or \'notAction\'/); }); -test('fails if key policy has no IAM principals', () => { +testFutureBehavior('fails if key policy has no IAM principals', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); const key = new kms.Key(stack, 'MyKey'); key.addToResourcePolicy(new iam.PolicyStatement({ @@ -410,14 +549,15 @@ test('fails if key policy has no IAM principals', () => { }); describe('imported keys', () => { - test('throw an error when providing something that is not a valid key ARN', () => { + testFutureBehavior('throw an error when providing something that is not a valid key ARN', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); expect(() => { kms.Key.fromKeyArn(stack, 'Imported', 'arn:aws:kms:us-east-1:123456789012:key'); }).toThrow(/KMS key ARN must be in the format 'arn:aws:kms:::key\/', got: 'arn:aws:kms:us-east-1:123456789012:key'/); }); - test('can have aliases added to them', () => { + testFutureBehavior('can have aliases added to them', flags, cdk.App, (app) => { const stack2 = new cdk.Stack(app, 'Stack2'); const myKeyImported = kms.Key.fromKeyArn(stack2, 'MyKeyImported', 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'); @@ -443,7 +583,8 @@ describe('imported keys', () => { describe('addToResourcePolicy allowNoOp and there is no policy', () => { // eslint-disable-next-line jest/expect-expect - test('succeed if set to true (default)', () => { + testFutureBehavior('succeed if set to true (default)', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); const key = kms.Key.fromKeyArn(stack, 'Imported', 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'); @@ -451,7 +592,8 @@ describe('addToResourcePolicy allowNoOp and there is no policy', () => { }); - test('fails if set to false', () => { + testFutureBehavior('fails if set to false', flags, cdk.App, (app) => { + const stack = new cdk.Stack(app); const key = kms.Key.fromKeyArn(stack, 'Imported', 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'); @@ -463,16 +605,8 @@ describe('addToResourcePolicy allowNoOp and there is no policy', () => { }); describe('when the defaultKeyPolicies feature flag is disabled', () => { - beforeEach(() => { - app = new cdk.App({ - context: { - '@aws-cdk/aws-kms:defaultKeyPolicies': false, - }, - }); - stack = new cdk.Stack(app); - }); - - test('default key policy', () => { + testLegacyBehavior('default key policy', cdk.App, (app) => { + const stack = new cdk.Stack(app); new kms.Key(stack, 'MyKey'); expect(stack).toHaveResource('AWS::KMS::Key', { @@ -496,7 +630,8 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { }, ResourcePart.CompleteDefinition); }); - test('policy if specified appends to the default key policy', () => { + testLegacyBehavior('policy if specified appends to the default key policy', cdk.App, (app) => { + const stack = new cdk.Stack(app); const key = new kms.Key(stack, 'MyKey'); const p = new iam.PolicyStatement({ resources: ['*'], actions: ['kms:Encrypt'] }); p.addArnPrincipal('arn:aws:iam::111122223333:root'); @@ -536,7 +671,8 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { }); }); - test('trustAccountIdentities changes key policy to allow IAM control', () => { + testLegacyBehavior('trustAccountIdentities changes key policy to allow IAM control', cdk.App, (app) => { + const stack = new cdk.Stack(app); new kms.Key(stack, 'MyKey', { trustAccountIdentities: true }); expect(stack).toHaveResourceLike('AWS::KMS::Key', { KeyPolicy: { @@ -554,7 +690,8 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { }); }); - test('additional key admins can be specified (with imported/immutable principal)', () => { + testLegacyBehavior('additional key admins can be specified (with imported/immutable principal)', cdk.App, (app) => { + const stack = new cdk.Stack(app); const adminRole = iam.Role.fromRoleArn(stack, 'Admin', 'arn:aws:iam::123456789012:role/TrustedAdmin'); new kms.Key(stack, 'MyKey', { admins: [adminRole] }); @@ -583,7 +720,8 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { }); }); - test('additional key admins can be specified (with owned/mutable principal)', () => { + testLegacyBehavior('additional key admins can be specified (with owned/mutable principal)', cdk.App, (app) => { + const stack = new cdk.Stack(app); const adminRole = new iam.Role(stack, 'AdminRole', { assumedBy: new iam.AccountRootPrincipal(), }); @@ -627,8 +765,9 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { }); describe('grants', () => { - test('grant decrypt on a key', () => { + testLegacyBehavior('grant decrypt on a key', cdk.App, (app) => { // GIVEN + const stack = new cdk.Stack(app); const key = new kms.Key(stack, 'Key'); const user = new iam.User(stack, 'User'); @@ -672,7 +811,7 @@ describe('when the defaultKeyPolicies feature flag is disabled', () => { }); }); - test('grant for a principal in a dependent stack works correctly', () => { + testLegacyBehavior('grant for a principal in a dependent stack works correctly', cdk.App, (app) => { const principalStack = new cdk.Stack(app, 'PrincipalStack'); const principal = new iam.Role(principalStack, 'Role', { assumedBy: new iam.AnyPrincipal(), diff --git a/packages/@aws-cdk/aws-lambda-destinations/lib/event-bridge.ts b/packages/@aws-cdk/aws-lambda-destinations/lib/event-bridge.ts index 893a0096f92fd..f61d8409da7bd 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/lib/event-bridge.ts +++ b/packages/@aws-cdk/aws-lambda-destinations/lib/event-bridge.ts @@ -1,6 +1,10 @@ import * as events from '@aws-cdk/aws-events'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Construct, Stack } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; /** * Use an Event Bridge event bus as a Lambda destination. diff --git a/packages/@aws-cdk/aws-lambda-destinations/lib/lambda.ts b/packages/@aws-cdk/aws-lambda-destinations/lib/lambda.ts index eaa6d020de3e7..319546471473d 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/lib/lambda.ts +++ b/packages/@aws-cdk/aws-lambda-destinations/lib/lambda.ts @@ -1,9 +1,13 @@ import * as events from '@aws-cdk/aws-events'; import * as targets from '@aws-cdk/aws-events-targets'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Construct } from '@aws-cdk/core'; + import { EventBridgeDestination } from './event-bridge'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Options for a Lambda destination */ diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json index afac04a03e5fb..53b599aa22c5a 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json @@ -176,7 +176,7 @@ "Properties": { "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", "Code": { - "ZipFile": "exports.handler = (event, context) => {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies\n const s3 = new (require('aws-sdk').S3)();\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const https = require('https');\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const url = require('url');\n log(JSON.stringify(event, undefined, 2));\n const props = event.ResourceProperties;\n if (event.RequestType === 'Delete') {\n props.NotificationConfiguration = {}; // this is how you clean out notifications\n }\n const req = {\n Bucket: props.BucketName,\n NotificationConfiguration: props.NotificationConfiguration,\n };\n return s3.putBucketNotificationConfiguration(req, (err, data) => {\n log({ err, data });\n if (err) {\n return submitResponse('FAILED', err.message + `\\nMore information in CloudWatch Log Stream: ${context.logStreamName}`);\n }\n else {\n return submitResponse('SUCCESS');\n }\n });\n function log(obj) {\n console.error(event.RequestId, event.StackId, event.LogicalResourceId, obj);\n }\n // eslint-disable-next-line max-len\n // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule\n // to allow sending an error messge as a reason.\n function submitResponse(responseStatus, reason) {\n const responseBody = JSON.stringify({\n Status: responseStatus,\n Reason: reason || 'See the details in CloudWatch Log Stream: ' + context.logStreamName,\n PhysicalResourceId: event.PhysicalResourceId || event.LogicalResourceId,\n StackId: event.StackId,\n RequestId: event.RequestId,\n LogicalResourceId: event.LogicalResourceId,\n NoEcho: false,\n });\n log({ responseBody });\n const parsedUrl = url.parse(event.ResponseURL);\n const options = {\n hostname: parsedUrl.hostname,\n port: 443,\n path: parsedUrl.path,\n method: 'PUT',\n headers: {\n 'content-type': '',\n 'content-length': responseBody.length,\n },\n };\n const request = https.request(options, (r) => {\n log({ statusCode: r.statusCode, statusMessage: r.statusMessage });\n context.done();\n });\n request.on('error', (error) => {\n log({ sendError: error });\n context.done();\n });\n request.write(responseBody);\n request.end();\n }\n};" + "ZipFile": "exports.handler = (event, context) => {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies\n const s3 = new (require('aws-sdk').S3)();\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const https = require('https');\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const url = require('url');\n log(JSON.stringify(event, undefined, 2));\n const props = event.ResourceProperties;\n if (event.RequestType === 'Delete') {\n props.NotificationConfiguration = {}; // this is how you clean out notifications\n }\n const req = {\n Bucket: props.BucketName,\n NotificationConfiguration: props.NotificationConfiguration,\n };\n return s3.putBucketNotificationConfiguration(req, (err, data) => {\n log({ err, data });\n if (err) {\n return submitResponse('FAILED', err.message + `\\nMore information in CloudWatch Log Stream: ${context.logStreamName}`);\n }\n else {\n return submitResponse('SUCCESS');\n }\n });\n function log(obj) {\n console.error(event.RequestId, event.StackId, event.LogicalResourceId, obj);\n }\n // eslint-disable-next-line max-len\n // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule\n // to allow sending an error message as a reason.\n function submitResponse(responseStatus, reason) {\n const responseBody = JSON.stringify({\n Status: responseStatus,\n Reason: reason || 'See the details in CloudWatch Log Stream: ' + context.logStreamName,\n PhysicalResourceId: event.PhysicalResourceId || event.LogicalResourceId,\n StackId: event.StackId,\n RequestId: event.RequestId,\n LogicalResourceId: event.LogicalResourceId,\n NoEcho: false,\n });\n log({ responseBody });\n const parsedUrl = url.parse(event.ResponseURL);\n const options = {\n hostname: parsedUrl.hostname,\n port: 443,\n path: parsedUrl.path,\n method: 'PUT',\n headers: {\n 'content-type': '',\n 'content-length': responseBody.length,\n },\n };\n const request = https.request(options, (r) => {\n log({ statusCode: r.statusCode, statusMessage: r.statusMessage });\n context.done();\n });\n request.on('error', (error) => {\n log({ sendError: error });\n context.done();\n });\n request.write(responseBody);\n request.end();\n }\n};" }, "Handler": "index.handler", "Role": { diff --git a/packages/@aws-cdk/aws-lambda-nodejs/package.json b/packages/@aws-cdk/aws-lambda-nodejs/package.json index eeb868b60073e..52238ca2ef897 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/package.json +++ b/packages/@aws-cdk/aws-lambda-nodejs/package.json @@ -66,8 +66,8 @@ "@aws-cdk/aws-ec2": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "delay": "4.4.0", - "esbuild": "^0.8.34", + "delay": "4.4.1", + "esbuild": "^0.8.44", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-lambda-python/lib/Dockerfile.dependencies b/packages/@aws-cdk/aws-lambda-python/lib/Dockerfile.dependencies index bfdb9e093ed4a..536887fd69a9a 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/Dockerfile.dependencies +++ b/packages/@aws-cdk/aws-lambda-python/lib/Dockerfile.dependencies @@ -6,6 +6,9 @@ FROM $IMAGE # Ensure rsync is installed RUN yum -q list installed rsync &>/dev/null || yum install -y rsync +# Upgrade pip (required by cryptography v3.4 and above, which is a dependency of poetry) +RUN pip install --upgrade pip + # Install pipenv and poetry so we can create a requirements.txt if we detect pipfile or poetry.lock respectively RUN pip install pipenv poetry diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json index c3adcd34e3e95..3690005685439 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersc677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57S3Bucket035B0B74" + "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3Bucket414E0E30" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57S3VersionKey781CC06F" + "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57S3VersionKey781CC06F" + "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098" } ] } @@ -72,13 +72,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "myhandlerServiceRole77891068", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python3.6" }, "DependsOn": [ @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParametersc677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57S3Bucket035B0B74": { + "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3Bucket414E0E30": { "Type": "String", - "Description": "S3 bucket for asset \"c677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57\"" + "Description": "S3 bucket for asset \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" }, - "AssetParametersc677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57S3VersionKey781CC06F": { + "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098": { "Type": "String", - "Description": "S3 key for asset version \"c677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57\"" + "Description": "S3 key for asset version \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" }, - "AssetParametersc677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57ArtifactHash70AD5A1E": { + "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353ArtifactHashECA6C88C": { "Type": "String", - "Description": "Artifact hash for asset \"c677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57\"" + "Description": "Artifact hash for asset \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.expected.json index 0c310b5f52e7e..ef1f355e528c3 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3BucketDF70124D" + "Ref": "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3BucketA501FC08" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3VersionKey530C68B0" + "Ref": "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3VersionKey1C3AFB39" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3VersionKey530C68B0" + "Ref": "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3VersionKey1C3AFB39" } ] } @@ -72,13 +72,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "myhandlerinlineServiceRole10C681F6", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python3.6" }, "DependsOn": [ @@ -121,7 +121,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3BucketB5A59BD8" + "Ref": "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3Bucket7DE4D4D5" }, "S3Key": { "Fn::Join": [ @@ -134,7 +134,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3VersionKey7657015C" + "Ref": "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3VersionKeyAEB67E87" } ] } @@ -147,7 +147,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3VersionKey7657015C" + "Ref": "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3VersionKeyAEB67E87" } ] } @@ -157,13 +157,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "myhandlerpython27ServiceRole2ED49C06", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python2.7" }, "DependsOn": [ @@ -206,7 +206,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3Bucket31144813" + "Ref": "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3BucketA66E9035" }, "S3Key": { "Fn::Join": [ @@ -219,7 +219,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3VersionKeyB48E8383" + "Ref": "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3VersionKeyAFEB5FDA" } ] } @@ -232,7 +232,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3VersionKeyB48E8383" + "Ref": "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3VersionKeyAFEB5FDA" } ] } @@ -242,13 +242,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "myhandlerpython38ServiceRole2049AFF7", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python3.8" }, "DependsOn": [ @@ -257,41 +257,41 @@ } }, "Parameters": { - "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3BucketDF70124D": { + "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3BucketA501FC08": { "Type": "String", - "Description": "S3 bucket for asset \"eef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255b\"" + "Description": "S3 bucket for asset \"94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45c\"" }, - "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3VersionKey530C68B0": { + "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3VersionKey1C3AFB39": { "Type": "String", - "Description": "S3 key for asset version \"eef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255b\"" + "Description": "S3 key for asset version \"94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45c\"" }, - "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bArtifactHashEE8E0CE9": { + "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cArtifactHash99DC751A": { "Type": "String", - "Description": "Artifact hash for asset \"eef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255b\"" + "Description": "Artifact hash for asset \"94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45c\"" }, - "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3BucketB5A59BD8": { + "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3Bucket7DE4D4D5": { "Type": "String", - "Description": "S3 bucket for asset \"f37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014\"" + "Description": "S3 bucket for asset \"3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28\"" }, - "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3VersionKey7657015C": { + "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3VersionKeyAEB67E87": { "Type": "String", - "Description": "S3 key for asset version \"f37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014\"" + "Description": "S3 key for asset version \"3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28\"" }, - "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014ArtifactHash7768674B": { + "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28ArtifactHashE51CE860": { "Type": "String", - "Description": "Artifact hash for asset \"f37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014\"" + "Description": "Artifact hash for asset \"3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28\"" }, - "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3Bucket31144813": { + "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3BucketA66E9035": { "Type": "String", - "Description": "S3 bucket for asset \"3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846a\"" + "Description": "S3 bucket for asset \"876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009\"" }, - "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3VersionKeyB48E8383": { + "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3VersionKeyAFEB5FDA": { "Type": "String", - "Description": "S3 key for asset version \"3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846a\"" + "Description": "S3 key for asset version \"876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009\"" }, - "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aArtifactHash652F614E": { + "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009ArtifactHashB9A1080D": { "Type": "String", - "Description": "Artifact hash for asset \"3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846a\"" + "Description": "Artifact hash for asset \"876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.poetry.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.poetry.expected.json index 0c310b5f52e7e..5ea17bca31920 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.poetry.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.poetry.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3BucketDF70124D" + "Ref": "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3BucketD53ED9C5" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3VersionKey530C68B0" + "Ref": "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3VersionKey3C218A3E" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3VersionKey530C68B0" + "Ref": "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3VersionKey3C218A3E" } ] } @@ -72,13 +72,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "myhandlerinlineServiceRole10C681F6", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python3.6" }, "DependsOn": [ @@ -121,7 +121,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3BucketB5A59BD8" + "Ref": "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3BucketFDE171D0" }, "S3Key": { "Fn::Join": [ @@ -134,7 +134,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3VersionKey7657015C" + "Ref": "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3VersionKey6209E240" } ] } @@ -147,7 +147,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3VersionKey7657015C" + "Ref": "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3VersionKey6209E240" } ] } @@ -157,13 +157,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "myhandlerpython27ServiceRole2ED49C06", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python2.7" }, "DependsOn": [ @@ -206,7 +206,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3Bucket31144813" + "Ref": "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3BucketA23E6312" }, "S3Key": { "Fn::Join": [ @@ -219,7 +219,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3VersionKeyB48E8383" + "Ref": "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3VersionKey1E21AF83" } ] } @@ -232,7 +232,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3VersionKeyB48E8383" + "Ref": "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3VersionKey1E21AF83" } ] } @@ -242,13 +242,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "myhandlerpython38ServiceRole2049AFF7", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python3.8" }, "DependsOn": [ @@ -257,41 +257,41 @@ } }, "Parameters": { - "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3BucketDF70124D": { + "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3BucketD53ED9C5": { "Type": "String", - "Description": "S3 bucket for asset \"eef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255b\"" + "Description": "S3 bucket for asset \"61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175ef\"" }, - "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bS3VersionKey530C68B0": { + "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3VersionKey3C218A3E": { "Type": "String", - "Description": "S3 key for asset version \"eef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255b\"" + "Description": "S3 key for asset version \"61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175ef\"" }, - "AssetParameterseef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255bArtifactHashEE8E0CE9": { + "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efArtifactHash6A1881A8": { "Type": "String", - "Description": "Artifact hash for asset \"eef17c074659b655f9b413019323db3976d06067e78d53c4e609ebe177ce255b\"" + "Description": "Artifact hash for asset \"61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175ef\"" }, - "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3BucketB5A59BD8": { + "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3BucketFDE171D0": { "Type": "String", - "Description": "S3 bucket for asset \"f37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014\"" + "Description": "S3 bucket for asset \"1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956d\"" }, - "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014S3VersionKey7657015C": { + "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3VersionKey6209E240": { "Type": "String", - "Description": "S3 key for asset version \"f37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014\"" + "Description": "S3 key for asset version \"1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956d\"" }, - "AssetParametersf37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014ArtifactHash7768674B": { + "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dArtifactHash02B929EC": { "Type": "String", - "Description": "Artifact hash for asset \"f37a4de97ca8831930cd2d0dc3f0962e653d756a118ce33271752a745489c014\"" + "Description": "Artifact hash for asset \"1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956d\"" }, - "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3Bucket31144813": { + "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3BucketA23E6312": { "Type": "String", - "Description": "S3 bucket for asset \"3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846a\"" + "Description": "S3 bucket for asset \"96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68\"" }, - "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aS3VersionKeyB48E8383": { + "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3VersionKey1E21AF83": { "Type": "String", - "Description": "S3 key for asset version \"3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846a\"" + "Description": "S3 key for asset version \"96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68\"" }, - "AssetParameters3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846aArtifactHash652F614E": { + "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68ArtifactHash0043D2A0": { "Type": "String", - "Description": "Artifact hash for asset \"3eb927f8df31281e22c710f842018fa10b0dde86f74f89313c9a27db6e75846a\"" + "Description": "Artifact hash for asset \"96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json index 5bc285e5c5769..9a81c901d7451 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json @@ -5,7 +5,7 @@ "Properties": { "Content": { "S3Bucket": { - "Ref": "AssetParameters314dd9f824ae895011cd7bb81d52a0ba316c902995491d7f4072c5aefccb6e4cS3Bucket6D2DF2A1" + "Ref": "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3BucketCCD07444" }, "S3Key": { "Fn::Join": [ @@ -18,7 +18,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters314dd9f824ae895011cd7bb81d52a0ba316c902995491d7f4072c5aefccb6e4cS3VersionKey897AD818" + "Ref": "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3VersionKeyA8B74284" } ] } @@ -31,7 +31,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters314dd9f824ae895011cd7bb81d52a0ba316c902995491d7f4072c5aefccb6e4cS3VersionKey897AD818" + "Ref": "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3VersionKeyA8B74284" } ] } @@ -118,19 +118,19 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "myhandlerServiceRole77891068", "Arn" ] }, - "Runtime": "python3.6", + "Handler": "index.handler", "Layers": [ { "Ref": "SharedDACC02AA" } - ] + ], + "Runtime": "python3.6" }, "DependsOn": [ "myhandlerServiceRole77891068" @@ -138,17 +138,17 @@ } }, "Parameters": { - "AssetParameters314dd9f824ae895011cd7bb81d52a0ba316c902995491d7f4072c5aefccb6e4cS3Bucket6D2DF2A1": { + "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3BucketCCD07444": { "Type": "String", - "Description": "S3 bucket for asset \"314dd9f824ae895011cd7bb81d52a0ba316c902995491d7f4072c5aefccb6e4c\"" + "Description": "S3 bucket for asset \"6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4a\"" }, - "AssetParameters314dd9f824ae895011cd7bb81d52a0ba316c902995491d7f4072c5aefccb6e4cS3VersionKey897AD818": { + "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3VersionKeyA8B74284": { "Type": "String", - "Description": "S3 key for asset version \"314dd9f824ae895011cd7bb81d52a0ba316c902995491d7f4072c5aefccb6e4c\"" + "Description": "S3 key for asset version \"6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4a\"" }, - "AssetParameters314dd9f824ae895011cd7bb81d52a0ba316c902995491d7f4072c5aefccb6e4cArtifactHashF8341E5E": { + "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aArtifactHashB3093591": { "Type": "String", - "Description": "Artifact hash for asset \"314dd9f824ae895011cd7bb81d52a0ba316c902995491d7f4072c5aefccb6e4c\"" + "Description": "Artifact hash for asset \"6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4a\"" }, "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3Bucket89C9DB12": { "Type": "String", diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json index 2f0a607ecaecb..b5b137205752f 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters428642c68731bb90502dd14a13320f1f31db649ea6f770f56acdc6f4bb1b381cS3BucketEEA58FD6" + "Ref": "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3BucketA9379638" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters428642c68731bb90502dd14a13320f1f31db649ea6f770f56acdc6f4bb1b381cS3VersionKey5B5DB95F" + "Ref": "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3VersionKey4376B462" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters428642c68731bb90502dd14a13320f1f31db649ea6f770f56acdc6f4bb1b381cS3VersionKey5B5DB95F" + "Ref": "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3VersionKey4376B462" } ] } @@ -72,13 +72,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "myhandlerServiceRole77891068", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python3.8" }, "DependsOn": [ @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParameters428642c68731bb90502dd14a13320f1f31db649ea6f770f56acdc6f4bb1b381cS3BucketEEA58FD6": { + "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3BucketA9379638": { "Type": "String", - "Description": "S3 bucket for asset \"428642c68731bb90502dd14a13320f1f31db649ea6f770f56acdc6f4bb1b381c\"" + "Description": "S3 bucket for asset \"1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148ad\"" }, - "AssetParameters428642c68731bb90502dd14a13320f1f31db649ea6f770f56acdc6f4bb1b381cS3VersionKey5B5DB95F": { + "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3VersionKey4376B462": { "Type": "String", - "Description": "S3 key for asset version \"428642c68731bb90502dd14a13320f1f31db649ea6f770f56acdc6f4bb1b381c\"" + "Description": "S3 key for asset version \"1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148ad\"" }, - "AssetParameters428642c68731bb90502dd14a13320f1f31db649ea6f770f56acdc6f4bb1b381cArtifactHash239A9708": { + "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adArtifactHashB9B928DC": { "Type": "String", - "Description": "Artifact hash for asset \"428642c68731bb90502dd14a13320f1f31db649ea6f770f56acdc6f4bb1b381c\"" + "Description": "Artifact hash for asset \"1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148ad\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json index ba8fee87727b4..6b3b8230c2874 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json @@ -72,13 +72,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "functionServiceRoleEF216095", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python2.7" }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json index 63e6ba74ccfe9..63fad4c61de14 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json @@ -296,7 +296,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersc677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57S3Bucket035B0B74" + "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3Bucket414E0E30" }, "S3Key": { "Fn::Join": [ @@ -309,7 +309,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57S3VersionKey781CC06F" + "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098" } ] } @@ -322,7 +322,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersc677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57S3VersionKey781CC06F" + "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098" } ] } @@ -332,13 +332,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "myhandlerServiceRole77891068", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python3.6", "VpcConfig": { "SecurityGroupIds": [ @@ -368,17 +368,17 @@ } }, "Parameters": { - "AssetParametersc677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57S3Bucket035B0B74": { + "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3Bucket414E0E30": { "Type": "String", - "Description": "S3 bucket for asset \"c677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57\"" + "Description": "S3 bucket for asset \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" }, - "AssetParametersc677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57S3VersionKey781CC06F": { + "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098": { "Type": "String", - "Description": "S3 key for asset version \"c677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57\"" + "Description": "S3 key for asset version \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" }, - "AssetParametersc677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57ArtifactHash70AD5A1E": { + "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353ArtifactHashECA6C88C": { "Type": "String", - "Description": "Artifact hash for asset \"c677eb7e524b9819a25fefd7267be6618341cd2b2d81f4b4aaa40911d698db57\"" + "Description": "Artifact hash for asset \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 5b7ee7cd3240e..de6940cd249e8 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -143,6 +143,19 @@ of a new version resource. You can specify options for this version through the > of code providers (such as `lambda.Code.fromBucket`) require that you define a > `lambda.Version` resource directly since the CDK is unable to determine if > their contents had changed. +> +> An alternative to defining a `lambda.Version` is to set an environment variable +> which changes at least as often as your code does. This makes sure the function +> always has the latest code. +> +> ```ts +> const codeVersion = "stringOrMethodToGetCodeVersion"; +> const fn = new lambda.Function(this, 'MyFunction', { +> environment: { +> 'CodeVersionString': codeVersion +> } +> }); +> ``` The `version.addAlias()` method can be used to define an AWS Lambda alias that points to a specific version. @@ -176,6 +189,17 @@ granting permissions to other AWS accounts or organizations. [Example of Lambda Layer usage](test/integ.layer-version.lit.ts) +By default, updating a layer creates a new layer version, and CloudFormation will delete the old version as part of the stack update. + +Alternatively, a removal policy can be used to retain the old version: + +```ts +import { LayerVersion } from '@aws-cdk/aws-lambda'; +new LayerVersion(this, 'MyLayer', { + removalPolicy: RemovalPolicy.RETAIN +}); +``` + ## Event Rule Target You can use an AWS Lambda function as a target for an Amazon CloudWatch event @@ -270,17 +294,24 @@ to learn more about AWS Lambda's X-Ray support. ## Lambda with Profiling +The following code configures the lambda function with CodeGuru profiling. By default, this creates a new CodeGuru +profiling group - + ```ts import * as lambda from '@aws-cdk/aws-lambda'; const fn = new lambda.Function(this, 'MyFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.PYTHON_3_6, handler: 'index.handler', - code: lambda.Code.fromInline('exports.handler = function(event, ctx, cb) { return cb(null, "hi"); }'), + code: lambda.Code.fromAsset('lambda-handler'), profiling: true }); ``` +The `profilingGroup` property can be used to configure an existing CodeGuru profiler group. + +CodeGuru profiling is supported for all Java runtimes and Python3.6+ runtimes. + See [the AWS documentation](https://docs.aws.amazon.com/codeguru/latest/profiler-ug/setting-up-lambda.html) to learn more about AWS Lambda's Profiling support. diff --git a/packages/@aws-cdk/aws-lambda/lib/destination.ts b/packages/@aws-cdk/aws-lambda/lib/destination.ts index 8fb6ab956db6d..8e2917ab827fc 100644 --- a/packages/@aws-cdk/aws-lambda/lib/destination.ts +++ b/packages/@aws-cdk/aws-lambda/lib/destination.ts @@ -1,6 +1,10 @@ -import { Construct } from '@aws-cdk/core'; + import { IFunction } from './function-base'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * A destination configuration */ diff --git a/packages/@aws-cdk/aws-lambda/lib/event-invoke-config.ts b/packages/@aws-cdk/aws-lambda/lib/event-invoke-config.ts index 6c2419c6f8fd5..52e1021cd6878 100644 --- a/packages/@aws-cdk/aws-lambda/lib/event-invoke-config.ts +++ b/packages/@aws-cdk/aws-lambda/lib/event-invoke-config.ts @@ -90,7 +90,7 @@ export class EventInvokeConfig extends Resource { : undefined, functionName: props.function.functionName, maximumEventAgeInSeconds: props.maxEventAge && props.maxEventAge.toSeconds(), - maximumRetryAttempts: props.retryAttempts !== undefined ? props.retryAttempts : undefined, + maximumRetryAttempts: props.retryAttempts ?? undefined, qualifier: props.qualifier || '$LATEST', }); } diff --git a/packages/@aws-cdk/aws-lambda/lib/function-base.ts b/packages/@aws-cdk/aws-lambda/lib/function-base.ts index 8b8dd585c21c2..92ca396cafbb2 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function-base.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function-base.ts @@ -326,7 +326,7 @@ export abstract class FunctionBase extends Resource implements IFunction { const permissionNode = this._functionNode().tryFindChild(identifier); if (!permissionNode) { throw new Error('Cannot modify permission to lambda function. Function is either imported or $LATEST version. ' - + 'If the function is imported from the same account use `fromFunctionAttributes()` API with the `allowPermissions` flag.'); + + 'If the function is imported from the same account use `fromFunctionAttributes()` API with the `sameEnvironment` flag.'); } return { statementAdded: true, policyDependable: permissionNode }; }, diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index 0e56ee695d4e5..fdcf4b4e0ec24 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -574,7 +574,7 @@ export class Function extends FunctionBase { let profilingGroupEnvironmentVariables: { [key: string]: string } = {}; if (props.profilingGroup && props.profiling !== false) { - this.validateProfilingEnvironmentVariables(props); + this.validateProfiling(props); props.profilingGroup.grantPublish(this.role); profilingGroupEnvironmentVariables = { AWS_CODEGURU_PROFILER_GROUP_ARN: Stack.of(scope).formatArn({ @@ -585,7 +585,7 @@ export class Function extends FunctionBase { AWS_CODEGURU_PROFILER_ENABLED: 'TRUE', }; } else if (props.profiling) { - this.validateProfilingEnvironmentVariables(props); + this.validateProfiling(props); const profilingGroup = new ProfilingGroup(this, 'ProfilingGroup', { computePlatform: ComputePlatform.AWS_LAMBDA, }); @@ -941,7 +941,10 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett }; } - private validateProfilingEnvironmentVariables(props: FunctionProps) { + private validateProfiling(props: FunctionProps) { + if (!props.runtime.supportsCodeGuruProfiling) { + throw new Error(`CodeGuru profiling is not supported by runtime ${props.runtime.name}`); + } if (props.environment && (props.environment.AWS_CODEGURU_PROFILER_GROUP_ARN || props.environment.AWS_CODEGURU_PROFILER_ENABLED)) { throw new Error('AWS_CODEGURU_PROFILER_GROUP_ARN and AWS_CODEGURU_PROFILER_ENABLED must not be set when profiling options enabled'); } diff --git a/packages/@aws-cdk/aws-lambda/lib/layers.ts b/packages/@aws-cdk/aws-lambda/lib/layers.ts index 92de56f57e7a8..babf91079b8b6 100644 --- a/packages/@aws-cdk/aws-lambda/lib/layers.ts +++ b/packages/@aws-cdk/aws-lambda/lib/layers.ts @@ -1,4 +1,4 @@ -import { IResource, Resource } from '@aws-cdk/core'; +import { IResource, RemovalPolicy, Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Code } from './code'; import { CfnLayerVersion, CfnLayerVersionPermission } from './lambda.generated'; @@ -28,6 +28,14 @@ export interface LayerVersionOptions { * @default - A name will be generated. */ readonly layerVersionName?: string; + + /** + * Whether to retain this version of the layer when a new version is added + * or when the stack is deleted. + * + * @default RemovalPolicy.DESTROY + */ + readonly removalPolicy?: RemovalPolicy; } export interface LayerVersionProps extends LayerVersionOptions { @@ -198,6 +206,10 @@ export class LayerVersion extends LayerVersionBase { licenseInfo: props.license, }); + if (props.removalPolicy) { + resource.applyRemovalPolicy(props.removalPolicy); + } + props.code.bindToResource(resource, { resourceProperty: 'Content', }); diff --git a/packages/@aws-cdk/aws-lambda/lib/private/scalable-function-attribute.ts b/packages/@aws-cdk/aws-lambda/lib/private/scalable-function-attribute.ts index 3e1c2e634e0b1..34ceb28ad861f 100644 --- a/packages/@aws-cdk/aws-lambda/lib/private/scalable-function-attribute.ts +++ b/packages/@aws-cdk/aws-lambda/lib/private/scalable-function-attribute.ts @@ -1,7 +1,11 @@ import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; -import { Construct, Token } from '@aws-cdk/core'; +import { Token } from '@aws-cdk/core'; import { IScalableFunctionAttribute, UtilizationScalingOptions } from '../scalable-attribute-api'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * A scalable lambda alias attribute */ diff --git a/packages/@aws-cdk/aws-lambda/lib/runtime.ts b/packages/@aws-cdk/aws-lambda/lib/runtime.ts index 1930641f45f04..3bbd8bcc43f6e 100644 --- a/packages/@aws-cdk/aws-lambda/lib/runtime.ts +++ b/packages/@aws-cdk/aws-lambda/lib/runtime.ts @@ -12,6 +12,12 @@ export interface LambdaRuntimeProps { * @default - the latest docker image "amazon/aws-sam-cli-build-image-" from https://hub.docker.com/u/amazon */ readonly bundlingDockerImage?: string; + + /** + * Whether this runtime is integrated with and supported for profiling using Amazon CodeGuru Profiler. + * @default false + */ + readonly supportsCodeGuruProfiling?: boolean; } export enum RuntimeFamily { @@ -37,28 +43,28 @@ export class Runtime { /** * The NodeJS runtime (nodejs) * - * @deprecated Use {@link NODEJS_10_X} + * @deprecated Use {@link NODEJS_12_X} */ public static readonly NODEJS = new Runtime('nodejs', RuntimeFamily.NODEJS, { supportsInlineCode: true }); /** * The NodeJS 4.3 runtime (nodejs4.3) * - * @deprecated Use {@link NODEJS_10_X} + * @deprecated Use {@link NODEJS_12_X} */ public static readonly NODEJS_4_3 = new Runtime('nodejs4.3', RuntimeFamily.NODEJS, { supportsInlineCode: true }); /** * The NodeJS 6.10 runtime (nodejs6.10) * - * @deprecated Use {@link NODEJS_10_X} + * @deprecated Use {@link NODEJS_12_X} */ public static readonly NODEJS_6_10 = new Runtime('nodejs6.10', RuntimeFamily.NODEJS, { supportsInlineCode: true }); /** * The NodeJS 8.10 runtime (nodejs8.10) * - * @deprecated Use {@link NODEJS_10_X} + * @deprecated Use {@link NODEJS_12_X} */ public static readonly NODEJS_8_10 = new Runtime('nodejs8.10', RuntimeFamily.NODEJS, { supportsInlineCode: true }); @@ -72,6 +78,11 @@ export class Runtime { */ public static readonly NODEJS_12_X = new Runtime('nodejs12.x', RuntimeFamily.NODEJS, { supportsInlineCode: true }); + /** + * The NodeJS 14.x runtime (nodejs14.x) + */ + public static readonly NODEJS_14_X = new Runtime('nodejs14.x', RuntimeFamily.NODEJS, { supportsInlineCode: false }); + /** * The Python 2.7 runtime (python2.7) */ @@ -80,32 +91,47 @@ export class Runtime { /** * The Python 3.6 runtime (python3.6) */ - public static readonly PYTHON_3_6 = new Runtime('python3.6', RuntimeFamily.PYTHON, { supportsInlineCode: true }); + public static readonly PYTHON_3_6 = new Runtime('python3.6', RuntimeFamily.PYTHON, { + supportsInlineCode: true, + supportsCodeGuruProfiling: true, + }); /** * The Python 3.7 runtime (python3.7) */ - public static readonly PYTHON_3_7 = new Runtime('python3.7', RuntimeFamily.PYTHON, { supportsInlineCode: true }); + public static readonly PYTHON_3_7 = new Runtime('python3.7', RuntimeFamily.PYTHON, { + supportsInlineCode: true, + supportsCodeGuruProfiling: true, + }); /** * The Python 3.8 runtime (python3.8) */ - public static readonly PYTHON_3_8 = new Runtime('python3.8', RuntimeFamily.PYTHON); + public static readonly PYTHON_3_8 = new Runtime('python3.8', RuntimeFamily.PYTHON, { + supportsInlineCode: true, + supportsCodeGuruProfiling: true, + }); /** * The Java 8 runtime (java8) */ - public static readonly JAVA_8 = new Runtime('java8', RuntimeFamily.JAVA); + public static readonly JAVA_8 = new Runtime('java8', RuntimeFamily.JAVA, { + supportsCodeGuruProfiling: true, + }); /** * The Java 8 Corretto runtime (java8.al2) */ - public static readonly JAVA_8_CORRETTO = new Runtime('java8.al2', RuntimeFamily.JAVA); + public static readonly JAVA_8_CORRETTO = new Runtime('java8.al2', RuntimeFamily.JAVA, { + supportsCodeGuruProfiling: true, + }); /** * The Java 11 runtime (java11) */ - public static readonly JAVA_11 = new Runtime('java11', RuntimeFamily.JAVA); + public static readonly JAVA_11 = new Runtime('java11', RuntimeFamily.JAVA, { + supportsCodeGuruProfiling: true, + }); /** * The .NET Core 1.0 runtime (dotnetcore1.0) @@ -178,6 +204,11 @@ export class Runtime { */ public readonly supportsInlineCode: boolean; + /** + * Whether this runtime is integrated with and supported for profiling using Amazon CodeGuru Profiler. + */ + public readonly supportsCodeGuruProfiling: boolean; + /** * The runtime family. */ @@ -194,6 +225,7 @@ export class Runtime { this.family = family; const imageName = props.bundlingDockerImage ?? `amazon/aws-sam-cli-build-image-${name}`; this.bundlingDockerImage = BundlingDockerImage.fromRegistry(imageName); + this.supportsCodeGuruProfiling = props.supportsCodeGuruProfiling ?? false; Runtime.ALL.push(this); } diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 3387ef50c2623..619dde4ff50fa 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -76,7 +76,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/aws-lambda": "^8.10.64", + "@types/aws-lambda": "^8.10.71", "@types/lodash": "^4.14.168", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/test/function.test.ts b/packages/@aws-cdk/aws-lambda/test/function.test.ts index 707b32428912b..51cfe70fd878c 100644 --- a/packages/@aws-cdk/aws-lambda/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/function.test.ts @@ -1611,7 +1611,7 @@ describe('function', () => { new lambda.Function(stack, 'MyLambda', { code: new lambda.InlineCode('foo'), handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.PYTHON_3_6, profiling: true, }); @@ -1660,7 +1660,7 @@ describe('function', () => { new lambda.Function(stack, 'MyLambda', { code: new lambda.InlineCode('foo'), handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.PYTHON_3_6, profilingGroup: new ProfilingGroup(stack, 'ProfilingGroup'), }); @@ -1712,7 +1712,7 @@ describe('function', () => { new lambda.Function(stack, 'MyLambda', { code: new lambda.InlineCode('foo'), handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.PYTHON_3_6, profiling: false, profilingGroup: new ProfilingGroup(stack, 'ProfilingGroup'), }); @@ -1743,7 +1743,7 @@ describe('function', () => { expect(() => new lambda.Function(stack, 'MyLambda', { code: new lambda.InlineCode('foo'), handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.PYTHON_3_6, profiling: true, environment: { AWS_CODEGURU_PROFILER_GROUP_ARN: 'profiler_group_arn', @@ -1758,7 +1758,7 @@ describe('function', () => { expect(() => new lambda.Function(stack, 'MyLambda', { code: new lambda.InlineCode('foo'), handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.PYTHON_3_6, profilingGroup: new ProfilingGroup(stack, 'ProfilingGroup'), environment: { AWS_CODEGURU_PROFILER_GROUP_ARN: 'profiler_group_arn', @@ -1766,6 +1766,20 @@ describe('function', () => { }, })).toThrow(/AWS_CODEGURU_PROFILER_GROUP_ARN and AWS_CODEGURU_PROFILER_ENABLED must not be set when profiling options enabled/); }); + + test('throws an error when used with an unsupported runtime', () => { + const stack = new cdk.Stack(); + expect(() => new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + profilingGroup: new ProfilingGroup(stack, 'ProfilingGroup'), + environment: { + AWS_CODEGURU_PROFILER_GROUP_ARN: 'profiler_group_arn', + AWS_CODEGURU_PROFILER_ENABLED: 'yes', + }, + })).toThrow(/not supported by runtime/); + }); }); describe('currentVersion', () => { diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json index 8bbe8cdef572a..30d39828cc39d 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.expected.json @@ -37,13 +37,13 @@ "Code": { "ZipFile": "exports.handler = async function(event) { return \"success\" }" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "NODEJS10XServiceRole2FD24B65", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -87,13 +87,13 @@ "Code": { "ZipFile": "exports.handler = async function(event) { return \"success\" }" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "NODEJS12XServiceRole59E71436", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs12.x" }, "DependsOn": [ @@ -137,13 +137,13 @@ "Code": { "ZipFile": "def handler(event, context):\n return \"success\"" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "PYTHON27ServiceRoleF484A17D", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python2.7" }, "DependsOn": [ @@ -187,13 +187,13 @@ "Code": { "ZipFile": "def handler(event, context):\n return \"success\"" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "PYTHON36ServiceRole814B3AD9", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python3.6" }, "DependsOn": [ @@ -237,18 +237,68 @@ "Code": { "ZipFile": "def handler(event, context):\n return \"success\"" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "PYTHON37ServiceRoleDE7E561E", "Arn" ] }, + "Handler": "index.handler", "Runtime": "python3.7" }, "DependsOn": [ "PYTHON37ServiceRoleDE7E561E" ] + }, + "PYTHON38ServiceRole3EA86BBE": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "PYTHON38A180AE47": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "def handler(event, context):\n return \"success\"" + }, + "Role": { + "Fn::GetAtt": [ + "PYTHON38ServiceRole3EA86BBE", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "python3.8" + }, + "DependsOn": [ + "PYTHON38ServiceRole3EA86BBE" + ] } }, "Outputs": { @@ -276,6 +326,11 @@ "Value": { "Ref": "PYTHON37D3A10E04" } + }, + "PYTHON38functionName": { + "Value": { + "Ref": "PYTHON38A180AE47" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts index aa4ef06e6a5e1..56f5bd27f7746 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts +++ b/packages/@aws-cdk/aws-lambda/test/integ.runtime.inlinecode.ts @@ -50,4 +50,11 @@ const python37 = new Function(stack, 'PYTHON_3_7', { }); new CfnOutput(stack, 'PYTHON_3_7-functionName', { value: python37.functionName }); -app.synth(); \ No newline at end of file +const python38 = new Function(stack, 'PYTHON_3_8', { + code: new InlineCode('def handler(event, context):\n return "success"'), + handler: 'index.handler', + runtime: Runtime.PYTHON_3_8, +}); +new CfnOutput(stack, 'PYTHON_3_8-functionName', { value: python38.functionName }); + +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda/test/layers.test.ts b/packages/@aws-cdk/aws-lambda/test/layers.test.ts index 0806f6d823dba..4a8f0e94ed6cb 100644 --- a/packages/@aws-cdk/aws-lambda/test/layers.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/layers.test.ts @@ -86,4 +86,21 @@ describe('layers', () => { }, }, ResourcePart.CompleteDefinition); }); + + test('creating a layer with a removal policy', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new lambda.LayerVersion(stack, 'layer', { + code: lambda.Code.fromAsset(path.join(__dirname, 'layer-code')), + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + + // THEN + expect(canonicalizeTemplate(SynthUtils.toCloudFormation(stack))).toHaveResource('AWS::Lambda::LayerVersion', { + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + }, ResourcePart.CompleteDefinition); + }); }); diff --git a/packages/@aws-cdk/aws-logs/lib/metric-filter.ts b/packages/@aws-cdk/aws-logs/lib/metric-filter.ts index 8dda5f1728fd3..e9b076b55c8a3 100644 --- a/packages/@aws-cdk/aws-logs/lib/metric-filter.ts +++ b/packages/@aws-cdk/aws-logs/lib/metric-filter.ts @@ -42,7 +42,7 @@ export class MetricFilter extends Resource { metricTransformations: [{ metricNamespace: props.metricNamespace, metricName: props.metricName, - metricValue: props.metricValue !== undefined ? props.metricValue : '1', + metricValue: props.metricValue ?? '1', defaultValue: props.defaultValue, }], }); diff --git a/packages/@aws-cdk/aws-logs/package.json b/packages/@aws-cdk/aws-logs/package.json index 2b1b897cd3785..98712b8960e29 100644 --- a/packages/@aws-cdk/aws-logs/package.json +++ b/packages/@aws-cdk/aws-logs/package.json @@ -72,15 +72,15 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.830.0", + "aws-sdk": "^2.842.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nock": "^13.0.5", + "nock": "^13.0.7", "nodeunit": "^0.11.3", "pkglint": "0.0.0", - "sinon": "^9.2.1" + "sinon": "^9.2.4" }, "dependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", diff --git a/packages/@aws-cdk/aws-lookoutvision/.eslintrc.js b/packages/@aws-cdk/aws-lookoutvision/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutvision/.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-lookoutvision/.gitignore b/packages/@aws-cdk/aws-lookoutvision/.gitignore new file mode 100644 index 0000000000000..62ebc95d75ce6 --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutvision/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js +junit.xml diff --git a/packages/@aws-cdk/aws-lookoutvision/.npmignore b/packages/@aws-cdk/aws-lookoutvision/.npmignore new file mode 100644 index 0000000000000..e4486030fcb17 --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutvision/.npmignore @@ -0,0 +1,28 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ diff --git a/packages/@aws-cdk/aws-lookoutvision/LICENSE b/packages/@aws-cdk/aws-lookoutvision/LICENSE new file mode 100644 index 0000000000000..28e4bdcec77ec --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutvision/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-2021 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-lookoutvision/NOTICE b/packages/@aws-cdk/aws-lookoutvision/NOTICE new file mode 100644 index 0000000000000..5fc3826926b5b --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutvision/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-lookoutvision/README.md b/packages/@aws-cdk/aws-lookoutvision/README.md new file mode 100644 index 0000000000000..b0082462251d2 --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutvision/README.md @@ -0,0 +1,20 @@ +# AWS::LookoutVision Construct Library + + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources]) are always stable and safe to use. +> +> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib + +--- + + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts +import lookoutvision = require('@aws-cdk/aws-lookoutvision'); +``` diff --git a/packages/@aws-cdk/aws-lookoutvision/jest.config.js b/packages/@aws-cdk/aws-lookoutvision/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutvision/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-lookoutvision/lib/index.ts b/packages/@aws-cdk/aws-lookoutvision/lib/index.ts new file mode 100644 index 0000000000000..1be824f5b354f --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutvision/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::LookoutVision CloudFormation Resources: +export * from './lookoutvision.generated'; diff --git a/packages/@aws-cdk/aws-lookoutvision/package.json b/packages/@aws-cdk/aws-lookoutvision/package.json new file mode 100644 index 0000000000000..9f7d59fd4c321 --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutvision/package.json @@ -0,0 +1,97 @@ +{ + "name": "@aws-cdk/aws-lookoutvision", + "version": "0.0.0", + "description": "The CDK Construct Library for AWS::LookoutVision", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "projectReferences": true, + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.LookoutVision", + "packageId": "Amazon.CDK.AWS.LookoutVision", + "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.lookoutvision", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "lookoutvision" + } + }, + "python": { + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ], + "distName": "aws-cdk.aws-lookoutvision", + "module": "aws_cdk.aws_lookoutvision" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-lookoutvision" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "compat": "cdk-compat", + "gen": "cfn2ts", + "rosetta:extract": "yarn --silent jsii-rosetta extract" + }, + "cdk-build": { + "cloudformation": "AWS::LookoutVision", + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": "true" + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::LookoutVision", + "aws-lookoutvision" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "cdk-build-tools": "0.0.0", + "cfn2ts": "0.0.0", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + } +} diff --git a/packages/@aws-cdk/aws-lookoutvision/test/lookoutvision.test.ts b/packages/@aws-cdk/aws-lookoutvision/test/lookoutvision.test.ts new file mode 100644 index 0000000000000..e394ef336bfb4 --- /dev/null +++ b/packages/@aws-cdk/aws-lookoutvision/test/lookoutvision.test.ts @@ -0,0 +1,6 @@ +import '@aws-cdk/assert/jest'; +import {} from '../lib'; + +test('No tests are specified for this package', () => { + expect(true).toBe(true); +}); diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts index 9c2645f64ab43..7e75162761a33 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts @@ -455,6 +455,8 @@ export class AuroraPostgresEngineVersion { public static readonly VER_11_8 = AuroraPostgresEngineVersion.of('11.8', '11', { s3Import: true, s3Export: true }); /** Version "11.9". */ public static readonly VER_11_9 = AuroraPostgresEngineVersion.of('11.9', '11', { s3Import: true, s3Export: true }); + /** Version "12.4". */ + public static readonly VER_12_4 = AuroraPostgresEngineVersion.of('12.4', '12', { s3Import: true, s3Export: true }); /** * Create a new AuroraPostgresEngineVersion with an arbitrary version. diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index e2e425eb71736..95d462c23f5fc 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -425,76 +425,181 @@ export interface PostgresEngineFeatures { * (those returned by {@link DatabaseInstanceEngine.postgres}). */ export class PostgresEngineVersion { - /** Version "9.5" (only a major version, without a specific minor version). */ + /** + * Version "9.5" (only a major version, without a specific minor version). + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5 = PostgresEngineVersion.of('9.5', '9.5'); - /** Version "9.5.2". */ + /** + * Version "9.5.2". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_2 = PostgresEngineVersion.of('9.5.2', '9.5'); - /** Version "9.5.4". */ + /** + * Version "9.5.4". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_4 = PostgresEngineVersion.of('9.5.4', '9.5'); - /** Version "9.5.6". */ + /** + * Version "9.5.6". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_6 = PostgresEngineVersion.of('9.5.6', '9.5'); - /** Version "9.5.7". */ + /** + * Version "9.5.7". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_7 = PostgresEngineVersion.of('9.5.7', '9.5'); - /** Version "9.5.9". */ + /** + * Version "9.5.9". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_9 = PostgresEngineVersion.of('9.5.9', '9.5'); - /** Version "9.5.10". */ + /** + * Version "9.5.10". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_10 = PostgresEngineVersion.of('9.5.10', '9.5'); - /** Version "9.5.12". */ + /** + * Version "9.5.12". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_12 = PostgresEngineVersion.of('9.5.12', '9.5'); - /** Version "9.5.13". */ + /** + * Version "9.5.13". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_13 = PostgresEngineVersion.of('9.5.13', '9.5'); - /** Version "9.5.14". */ + /** + * Version "9.5.14". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_14 = PostgresEngineVersion.of('9.5.14', '9.5'); - /** Version "9.5.15". */ + /** + * Version "9.5.15". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_15 = PostgresEngineVersion.of('9.5.15', '9.5'); - /** Version "9.5.16". */ + /** + * Version "9.5.16". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_16 = PostgresEngineVersion.of('9.5.16', '9.5'); - /** Version "9.5.18". */ + /** + * Version "9.5.18". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_18 = PostgresEngineVersion.of('9.5.18', '9.5'); - /** Version "9.5.19". */ + /** + * Version "9.5.19". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_19 = PostgresEngineVersion.of('9.5.19', '9.5'); - /** Version "9.5.20". */ + /** + * Version "9.5.20". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_20 = PostgresEngineVersion.of('9.5.20', '9.5'); - /** Version "9.5.21". */ + /** + * Version "9.5.21". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_21 = PostgresEngineVersion.of('9.5.21', '9.5'); - /** Version "9.5.22". */ + /** + * Version "9.5.22". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_22 = PostgresEngineVersion.of('9.5.22', '9.5'); - /** Version "9.5.23". */ + /** + * Version "9.5.23". + * @deprecated PostgreSQL 9.5 will reach end of life on February 16, 2021 + */ public static readonly VER_9_5_23 = PostgresEngineVersion.of('9.5.23', '9.5'); - /** Version "9.6" (only a major version, without a specific minor version). */ + /** + * Version "9.6" (only a major version, without a specific minor version). + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6 = PostgresEngineVersion.of('9.6', '9.6'); - /** Version "9.6.1". */ + /** + * Version "9.6.1". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_1 = PostgresEngineVersion.of('9.6.1', '9.6'); - /** Version "9.6.2". */ + /** + * Version "9.6.2". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_2 = PostgresEngineVersion.of('9.6.2', '9.6'); - /** Version "9.6.3". */ + /** + * Version "9.6.3". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_3 = PostgresEngineVersion.of('9.6.3', '9.6'); - /** Version "9.6.5". */ + /** + * Version "9.6.5". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_5 = PostgresEngineVersion.of('9.6.5', '9.6'); - /** Version "9.6.6". */ + /** + * Version "9.6.6". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_6 = PostgresEngineVersion.of('9.6.6', '9.6'); - /** Version "9.6.8". */ + /** + * Version "9.6.8". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_8 = PostgresEngineVersion.of('9.6.8', '9.6'); - /** Version "9.6.9". */ + /** + * Version "9.6.9". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_9 = PostgresEngineVersion.of('9.6.9', '9.6'); - /** Version "9.6.10". */ + /** + * Version "9.6.10". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_10 = PostgresEngineVersion.of('9.6.10', '9.6'); - /** Version "9.6.11". */ + /** + * Version "9.6.11". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_11 = PostgresEngineVersion.of('9.6.11', '9.6'); - /** Version "9.6.12". */ + /** + * Version "9.6.12". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_12 = PostgresEngineVersion.of('9.6.12', '9.6'); - /** Version "9.6.14". */ + /** + * Version "9.6.14". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_14 = PostgresEngineVersion.of('9.6.14', '9.6'); - /** Version "9.6.15". */ + /** + * Version "9.6.15". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_15 = PostgresEngineVersion.of('9.6.15', '9.6'); - /** Version "9.6.16". */ + /** + * Version "9.6.16". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_16 = PostgresEngineVersion.of('9.6.16', '9.6'); - /** Version "9.6.17". */ + /** + * Version "9.6.17". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_17 = PostgresEngineVersion.of('9.6.17', '9.6'); - /** Version "9.6.18". */ + /** + * Version "9.6.18". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_18 = PostgresEngineVersion.of('9.6.18', '9.6'); - /** Version "9.6.19". */ + /** + * Version "9.6.19". + * @deprecated PostgreSQL 9.6 will reach end of life in November 2021 + */ public static readonly VER_9_6_19 = PostgresEngineVersion.of('9.6.19', '9.6'); /** Version "10" (only a major version, without a specific minor version). */ @@ -551,6 +656,8 @@ export class PostgresEngineVersion { public static readonly VER_12_3 = PostgresEngineVersion.of('12.3', '12', { s3Import: true }); /** Version "12.4". */ public static readonly VER_12_4 = PostgresEngineVersion.of('12.4', '12', { s3Import: true }); + /** Version "12.5". */ + public static readonly VER_12_5 = PostgresEngineVersion.of('12.5', '12', { s3Import: true }); /** * Create a new PostgresEngineVersion with an arbitrary version. diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 74a28666496d2..cb4c3d3487a8c 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -690,8 +690,8 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData this.newCfnProps = { autoMinorVersionUpgrade: props.autoMinorVersionUpgrade, availabilityZone: props.multiAz ? undefined : props.availabilityZone, - backupRetentionPeriod: props.backupRetention ? props.backupRetention.toDays() : undefined, - copyTagsToSnapshot: props.copyTagsToSnapshot !== undefined ? props.copyTagsToSnapshot : true, + backupRetentionPeriod: props.backupRetention?.toDays(), + copyTagsToSnapshot: props.copyTagsToSnapshot ?? true, dbInstanceClass: Lazy.string({ produce: () => `db.${this.instanceType}` }), dbInstanceIdentifier: props.instanceIdentifier, dbSubnetGroupName: subnetGroup.subnetGroupName, @@ -701,15 +701,15 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData enableIamDatabaseAuthentication: Lazy.any({ produce: () => this.enableIamAuthentication }), enablePerformanceInsights: enablePerformanceInsights || props.enablePerformanceInsights, // fall back to undefined if not set, iops, - monitoringInterval: props.monitoringInterval && props.monitoringInterval.toSeconds(), - monitoringRoleArn: monitoringRole && monitoringRole.roleArn, + monitoringInterval: props.monitoringInterval?.toSeconds(), + monitoringRoleArn: monitoringRole?.roleArn, multiAz: props.multiAz, optionGroupName: props.optionGroup?.optionGroupName, performanceInsightsKmsKeyId: props.performanceInsightEncryptionKey?.keyArn, performanceInsightsRetentionPeriod: enablePerformanceInsights ? (props.performanceInsightRetention || PerformanceInsightRetention.DEFAULT) : undefined, - port: props.port ? props.port.toString() : undefined, + port: props.port?.toString(), preferredBackupWindow: props.preferredBackupWindow, preferredMaintenanceWindow: props.preferredMaintenanceWindow, processorFeatures: props.processorFeatures && renderProcessorFeatures(props.processorFeatures), @@ -849,7 +849,7 @@ abstract class DatabaseInstanceSource extends DatabaseInstanceNew implements IDa ...this.newCfnProps, associatedRoles: instanceAssociatedRoles.length > 0 ? instanceAssociatedRoles : undefined, optionGroupName: engineConfig.optionGroup?.optionGroupName, - allocatedStorage: props.allocatedStorage ? props.allocatedStorage.toString() : '100', + allocatedStorage: props.allocatedStorage?.toString() ?? '100', allowMajorVersionUpgrade: props.allowMajorVersionUpgrade, dbName: props.databaseName, dbParameterGroupName: instanceParameterGroupConfig?.parameterGroupName, @@ -1041,9 +1041,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme const instance = new CfnDBInstance(this, 'Resource', { ...this.sourceCfnProps, dbSnapshotIdentifier: props.snapshotIdentifier, - masterUserPassword: secret - ? secret.secretValueFromJson('password').toString() - : credentials?.password?.toString(), + masterUserPassword: secret?.secretValueFromJson('password')?.toString() ?? credentials?.password?.toString(), }); this.instanceIdentifier = instance.ref; @@ -1117,7 +1115,7 @@ export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements ...this.newCfnProps, // this must be ARN, not ID, because of https://github.com/terraform-providers/terraform-provider-aws/issues/528#issuecomment-391169012 sourceDbInstanceIdentifier: props.sourceDatabaseInstance.instanceArn, - kmsKeyId: props.storageEncryptionKey && props.storageEncryptionKey.keyArn, + kmsKeyId: props.storageEncryptionKey?.keyArn, storageEncrypted: props.storageEncryptionKey ? true : props.storageEncrypted, }); diff --git a/packages/@aws-cdk/aws-rds/lib/private/util.ts b/packages/@aws-cdk/aws-rds/lib/private/util.ts index 8cba1e4a1ee1e..f97486b5daddf 100644 --- a/packages/@aws-cdk/aws-rds/lib/private/util.ts +++ b/packages/@aws-cdk/aws-rds/lib/private/util.ts @@ -1,10 +1,14 @@ import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; -import { Construct, CfnDeletionPolicy, CfnResource, RemovalPolicy } from '@aws-cdk/core'; +import { CfnDeletionPolicy, CfnResource, RemovalPolicy } from '@aws-cdk/core'; import { DatabaseSecret } from '../database-secret'; import { IEngine } from '../engine'; import { Credentials } from '../props'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * The default set of characters we exclude from generated passwords for database users. * It's a combination of characters that have a tendency to cause problems in shell scripts, @@ -88,9 +92,7 @@ export function applyRemovalPolicy(cfnDatabase: CfnResource, removalPolicy?: Rem * Enable if explicitly provided or if the RemovalPolicy has been set to RETAIN */ export function defaultDeletionProtection(deletionProtection?: boolean, removalPolicy?: RemovalPolicy): boolean | undefined { - return deletionProtection !== undefined - ? deletionProtection - : (removalPolicy === RemovalPolicy.RETAIN ? true : undefined); + return deletionProtection ?? (removalPolicy === RemovalPolicy.RETAIN ? true : undefined); } /** diff --git a/packages/@aws-cdk/aws-rds/test/test.instance.ts b/packages/@aws-cdk/aws-rds/test/test.instance.ts index 7591a576946cb..608e3baf5c926 100644 --- a/packages/@aws-cdk/aws-rds/test/test.instance.ts +++ b/packages/@aws-cdk/aws-rds/test/test.instance.ts @@ -1023,7 +1023,7 @@ export = { new rds.DatabaseInstance(stack, 'Instance', { vpc, engine: rds.DatabaseInstanceEngine.postgres({ - version: rds.PostgresEngineVersion.VER_9_5_7, + version: rds.PostgresEngineVersion.VER_12_4, }), }); diff --git a/packages/@aws-cdk/aws-redshift/lib/cluster.ts b/packages/@aws-cdk/aws-redshift/lib/cluster.ts index 9126ef4fda737..a94bfa31b7fb5 100644 --- a/packages/@aws-cdk/aws-redshift/lib/cluster.ts +++ b/packages/@aws-cdk/aws-redshift/lib/cluster.ts @@ -40,6 +40,14 @@ export enum NodeType { * dc2.8xlarge */ DC2_8XLARGE = 'dc2.8xlarge', + /** + * ra3.xlplus + */ + RA3_XLPLUS = 'ra3.xlplus', + /** + * ra3.4xlarge + */ + RA3_4XLARGE = 'ra3.4xlarge', /** * ra3.16xlarge */ @@ -406,11 +414,11 @@ export class Cluster extends ClusterBase { super(scope, id); this.vpc = props.vpc; - this.vpcSubnets = props.vpcSubnets ? props.vpcSubnets : { + this.vpcSubnets = props.vpcSubnets ?? { subnetType: ec2.SubnetType.PRIVATE, }; - const removalPolicy = props.removalPolicy ? props.removalPolicy : RemovalPolicy.RETAIN; + const removalPolicy = props.removalPolicy ?? RemovalPolicy.RETAIN; const subnetGroup = props.subnetGroup ?? new ClusterSubnetGroup(this, 'Subnets', { description: `Subnets for ${id} Redshift cluster`, @@ -419,11 +427,10 @@ export class Cluster extends ClusterBase { removalPolicy: removalPolicy, }); - const securityGroups = props.securityGroups !== undefined ? - props.securityGroups : [new ec2.SecurityGroup(this, 'SecurityGroup', { - description: 'Redshift security group', - vpc: this.vpc, - })]; + const securityGroups = props.securityGroups ?? [new ec2.SecurityGroup(this, 'SecurityGroup', { + description: 'Redshift security group', + vpc: this.vpc, + })]; const securityGroupIds = securityGroups.map(sg => sg.securityGroupId); @@ -464,22 +471,20 @@ export class Cluster extends ClusterBase { port: props.port, clusterParameterGroupName: props.parameterGroup && props.parameterGroup.clusterParameterGroupName, // Admin - masterUsername: secret ? secret.secretValueFromJson('username').toString() : props.masterUser.masterUsername, - masterUserPassword: secret - ? secret.secretValueFromJson('password').toString() - : (props.masterUser.masterPassword - ? props.masterUser.masterPassword.toString() - : 'default'), + masterUsername: secret?.secretValueFromJson('username').toString() ?? props.masterUser.masterUsername, + masterUserPassword: secret?.secretValueFromJson('password').toString() + ?? props.masterUser.masterPassword?.toString() + ?? 'default', preferredMaintenanceWindow: props.preferredMaintenanceWindow, nodeType: props.nodeType || NodeType.DC2_LARGE, numberOfNodes: nodeCount, loggingProperties, - iamRoles: props.roles ? props.roles.map(role => role.roleArn) : undefined, + iamRoles: props?.roles?.map(role => role.roleArn), dbName: props.defaultDatabaseName || 'default_db', publiclyAccessible: props.publiclyAccessible || false, // Encryption kmsKeyId: props.encryptionKey && props.encryptionKey.keyArn, - encrypted: props.encrypted !== undefined ? props.encrypted : true, + encrypted: props.encrypted ?? true, }); cluster.applyRemovalPolicy(removalPolicy, { diff --git a/packages/@aws-cdk/aws-route53/.gitignore b/packages/@aws-cdk/aws-route53/.gitignore index 86fc837df8fca..a82230b5888d0 100644 --- a/packages/@aws-cdk/aws-route53/.gitignore +++ b/packages/@aws-cdk/aws-route53/.gitignore @@ -15,4 +15,5 @@ nyc.config.js *.snk !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/.npmignore b/packages/@aws-cdk/aws-route53/.npmignore index a94c531529866..9e88226921c33 100644 --- a/packages/@aws-cdk/aws-route53/.npmignore +++ b/packages/@aws-cdk/aws-route53/.npmignore @@ -23,4 +23,5 @@ tsconfig.json # exclude cdk artifacts **/cdk.out junit.xml -test/ \ No newline at end of file +test/ +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/README.md b/packages/@aws-cdk/aws-route53/README.md index 116dd217748cd..b9f75fb1b9d4e 100644 --- a/packages/@aws-cdk/aws-route53/README.md +++ b/packages/@aws-cdk/aws-route53/README.md @@ -60,7 +60,7 @@ new route53.TxtRecord(this, 'TXTRecord', { }); ``` -To add a A record to your zone: +To add an A record to your zone: ```ts import * as route53 from '@aws-cdk/aws-route53'; @@ -71,7 +71,28 @@ new route53.ARecord(this, 'ARecord', { }); ``` -To add a AAAA record pointing to a CloudFront distribution: +To add an A record for an EC2 instance with an Elastic IP (EIP) to your zone: + +```ts +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as route53 from '@aws-cdk/aws-route53'; + +const instance = new ec2.Instance(this, 'Instance', { + // ... +}); + +const elasticIp = new ec2.CfnEIP(this, 'EIP', { + domain: 'vpc', + instanceId: instance.instanceId +}); + +new route53.ARecord(this, 'ARecord', { + zone: myZone, + target: route53.RecordTarget.fromIpAddresses(elasticIp.ref) +}); +``` + +To add an AAAA record pointing to a CloudFront distribution: ```ts import * as route53 from '@aws-cdk/aws-route53'; @@ -88,6 +109,29 @@ Constructs are available for A, AAAA, CAA, CNAME, MX, NS, SRV and TXT records. Use the `CaaAmazonRecord` construct to easily restrict certificate authorities allowed to issue certificates for a domain to Amazon only. +To add a NS record to a HostedZone in different account + +```ts +import * as route53 from '@aws-cdk/aws-route53'; + +// In the account containing the HostedZone +const parentZone = new route53.PublicHostedZone(this, 'HostedZone', { + zoneName: 'someexample.com', + crossAccountZoneDelegationPrinciple: new iam.AccountPrincipal('12345678901') +}); + +// In this account +const subZone = new route53.PublicHostedZone(this, 'SubZone', { + zoneName: 'sub.someexample.com' +}); + +new route53.CrossAccountZoneDelegationRecord(this, 'delegate', { + delegatedZone: subZone, + parentHostedZoneId: parentZone.hostedZoneId, + delegationRole: parentZone.crossAccountDelegationRole +}); +``` + ## Imports If you don't know the ID of the Hosted Zone to import, you can use the @@ -125,9 +169,7 @@ Alternatively, use the `HostedZone.fromHostedZoneId` to import hosted zones if you know the ID and the retrieval for the `zoneName` is undesirable. ```ts -const zone = HostedZone.fromHostedZoneId(this, 'MyZone', { - hostedZoneId: 'ZOJJZC49E0EPZ', -}); +const zone = HostedZone.fromHostedZoneId(this, 'MyZone', 'ZOJJZC49E0EPZ'); ``` ## VPC Endpoint Service Private DNS diff --git a/packages/@aws-cdk/aws-route53/jest.config.js b/packages/@aws-cdk/aws-route53/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-route53/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-route53/lib/cross-account-zone-delegation-handler/index.ts b/packages/@aws-cdk/aws-route53/lib/cross-account-zone-delegation-handler/index.ts new file mode 100644 index 0000000000000..3c711d283d7e5 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/lib/cross-account-zone-delegation-handler/index.ts @@ -0,0 +1,66 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { Credentials, Route53, STS } from 'aws-sdk'; + +interface ResourceProperties { + AssumeRoleArn: string, + ParentZoneId: string, + DelegatedZoneName: string, + DelegatedZoneNameServers: string[], + TTL: number, +} + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { + const resourceProps = event.ResourceProperties as unknown as ResourceProperties; + + switch (event.RequestType) { + case 'Create': + case 'Update': + return cfnEventHandler(resourceProps, false); + case 'Delete': + return cfnEventHandler(resourceProps, true); + } +} + +async function cfnEventHandler(props: ResourceProperties, isDeleteEvent: boolean) { + const { AssumeRoleArn, ParentZoneId, DelegatedZoneName, DelegatedZoneNameServers, TTL } = props; + + const credentials = await getCrossAccountCredentials(AssumeRoleArn); + const route53 = new Route53({ credentials }); + + await route53.changeResourceRecordSets({ + HostedZoneId: ParentZoneId, + ChangeBatch: { + Changes: [{ + Action: isDeleteEvent ? 'DELETE' : 'UPSERT', + ResourceRecordSet: { + Name: DelegatedZoneName, + Type: 'NS', + TTL, + ResourceRecords: DelegatedZoneNameServers.map(ns => ({ Value: ns })), + }, + }], + }, + }).promise(); +} + +async function getCrossAccountCredentials(roleArn: string): Promise { + const sts = new STS(); + const timestamp = (new Date()).getTime(); + + const { Credentials: assumedCredentials } = await sts + .assumeRole({ + RoleArn: roleArn, + RoleSessionName: `cross-account-zone-delegation-${timestamp}`, + }) + .promise(); + + if (!assumedCredentials) { + throw Error('Error getting assume role credentials'); + } + + return new Credentials({ + accessKeyId: assumedCredentials.AccessKeyId, + secretAccessKey: assumedCredentials.SecretAccessKey, + sessionToken: assumedCredentials.SessionToken, + }); +} diff --git a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts index f11e9ae180e7f..1ca835cb258d9 100644 --- a/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts +++ b/packages/@aws-cdk/aws-route53/lib/hosted-zone.ts @@ -1,4 +1,5 @@ import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { ContextProvider, Duration, Lazy, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; @@ -190,6 +191,13 @@ export interface PublicHostedZoneProps extends CommonHostedZoneProps { * @default false */ readonly caaAmazon?: boolean; + + /** + * A principal which is trusted to assume a role for zone delegation + * + * @default - No delegation configuration + */ + readonly crossAccountZoneDelegationPrincipal?: iam.IPrincipal; } /** @@ -222,6 +230,11 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone { return new Import(scope, id); } + /** + * Role for cross account zone delegation + */ + public readonly crossAccountZoneDelegationRole?: iam.Role; + constructor(scope: Construct, id: string, props: PublicHostedZoneProps) { super(scope, id, props); @@ -230,6 +243,22 @@ export class PublicHostedZone extends HostedZone implements IPublicHostedZone { zone: this, }); } + + if (props.crossAccountZoneDelegationPrincipal) { + this.crossAccountZoneDelegationRole = new iam.Role(this, 'CrossAccountZoneDelegationRole', { + assumedBy: props.crossAccountZoneDelegationPrincipal, + inlinePolicies: { + delegation: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + actions: ['route53:ChangeResourceRecordSets'], + resources: [this.hostedZoneArn], + }), + ], + }), + }, + }); + } } public addVpc(_vpc: ec2.IVpc) { diff --git a/packages/@aws-cdk/aws-route53/lib/record-set.ts b/packages/@aws-cdk/aws-route53/lib/record-set.ts index 3111375a8682d..577af5c1a3a57 100644 --- a/packages/@aws-cdk/aws-route53/lib/record-set.ts +++ b/packages/@aws-cdk/aws-route53/lib/record-set.ts @@ -1,10 +1,18 @@ -import { Duration, IResource, Resource, Token } from '@aws-cdk/core'; +import * as path from 'path'; +import * as iam from '@aws-cdk/aws-iam'; +import { CustomResource, CustomResourceProvider, CustomResourceProviderRuntime, Duration, IResource, Resource, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { IAliasRecordTarget } from './alias-record-target'; import { IHostedZone } from './hosted-zone-ref'; import { CfnRecordSet } from './route53.generated'; import { determineFullyQualifiedDomainName } from './util'; +const CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE = 'Custom::CrossAccountZoneDelegation'; + +// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. +// eslint-disable-next-line +import { Construct as CoreConstruct } from '@aws-cdk/core'; + /** * A record set */ @@ -559,3 +567,57 @@ export class ZoneDelegationRecord extends RecordSet { }); } } + +/** + * Construction properties for a CrossAccountZoneDelegationRecord + */ +export interface CrossAccountZoneDelegationRecordProps { + /** + * The zone to be delegated + */ + readonly delegatedZone: IHostedZone; + + /** + * The hosted zone id in the parent account + */ + readonly parentHostedZoneId: string; + + /** + * The delegation role in the parent account + */ + readonly delegationRole: iam.IRole; + + /** + * The resource record cache time to live (TTL). + * + * @default Duration.days(2) + */ + readonly ttl?: Duration; +} + +/** + * A Cross Account Zone Delegation record + */ +export class CrossAccountZoneDelegationRecord extends CoreConstruct { + constructor(scope: Construct, id: string, props: CrossAccountZoneDelegationRecordProps) { + super(scope, id); + + const serviceToken = CustomResourceProvider.getOrCreate(this, CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE, { + codeDirectory: path.join(__dirname, 'cross-account-zone-delegation-handler'), + runtime: CustomResourceProviderRuntime.NODEJS_12, + policyStatements: [{ Effect: 'Allow', Action: 'sts:AssumeRole', Resource: props.delegationRole.roleArn }], + }); + + new CustomResource(this, 'CrossAccountZoneDelegationCustomResource', { + resourceType: CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE, + serviceToken, + properties: { + AssumeRoleArn: props.delegationRole.roleArn, + ParentZoneId: props.parentHostedZoneId, + DelegatedZoneName: props.delegatedZone.zoneName, + DelegatedZoneNameServers: props.delegatedZone.hostedZoneNameServers!, + TTL: (props.ttl || Duration.days(2)).toSeconds(), + }, + }); + } +} diff --git a/packages/@aws-cdk/aws-route53/lib/util.ts b/packages/@aws-cdk/aws-route53/lib/util.ts index a316e2ecd59f9..d703c77348538 100644 --- a/packages/@aws-cdk/aws-route53/lib/util.ts +++ b/packages/@aws-cdk/aws-route53/lib/util.ts @@ -1,6 +1,10 @@ -import { Construct, Stack } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { IHostedZone } from './hosted-zone-ref'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Validates a zone name is valid by Route53 specifc naming rules, * and that there is no trailing dot in the name. diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index 89990b82cc61f..4fe0450261da2 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -55,7 +55,8 @@ "cloudformation": "AWS::Route53", "env": { "AWSLINT_BASE_CONSTRUCT": true - } + }, + "jest": true }, "keywords": [ "aws", @@ -72,16 +73,17 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.830.0", + "aws-sdk": "^2.842.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", + "nodeunit-shim": "0.0.0", "pkglint": "0.0.0" }, "dependencies": { "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", @@ -91,6 +93,7 @@ "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts b/packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts new file mode 100644 index 0000000000000..8c8dcafcbd9c7 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/cross-account-zone-delegation-handler/index.test.ts @@ -0,0 +1,119 @@ +import { handler } from '../../lib/cross-account-zone-delegation-handler'; + +const mockStsClient = { + assumeRole: jest.fn().mockReturnThis(), + promise: jest.fn(), +}; +const mockRoute53Client = { + changeResourceRecordSets: jest.fn().mockReturnThis(), + promise: jest.fn(), +}; + +jest.mock('aws-sdk', () => { + return { + ...(jest.requireActual('aws-sdk') as any), + STS: jest.fn(() => mockStsClient), + Route53: jest.fn(() => mockRoute53Client), + }; +}); + +beforeEach(() => { + mockStsClient.assumeRole.mockReturnThis(); + mockRoute53Client.changeResourceRecordSets.mockReturnThis(); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +test('throws error if getting credentials fails', async () => { + // GIVEN + mockStsClient.promise.mockResolvedValueOnce({ Credentials: undefined }); + + // WHEN + const event= getCfnEvent(); + + // THEN + await expect(invokeHandler(event)).rejects.toThrow(/Error getting assume role credentials/); + + expect(mockStsClient.assumeRole).toHaveBeenCalledTimes(1); + expect(mockStsClient.assumeRole).toHaveBeenCalledWith({ + RoleArn: 'roleArn', + RoleSessionName: expect.any(String), + }); +}); + +test('calls create resouce record set with Upsert for Create event', async () => { + // GIVEN + mockStsClient.promise.mockResolvedValueOnce({ Credentials: { AccessKeyId: 'K', SecretAccessKey: 'S', SessionToken: 'T' } }); + mockRoute53Client.promise.mockResolvedValueOnce({}); + + // WHEN + const event= getCfnEvent(); + await invokeHandler(event); + + // THEN + expect(mockRoute53Client.changeResourceRecordSets).toHaveBeenCalledTimes(1); + expect(mockRoute53Client.changeResourceRecordSets).toHaveBeenCalledWith({ + HostedZoneId: '1', + ChangeBatch: { + Changes: [{ + Action: 'UPSERT', + ResourceRecordSet: { + Name: 'recordName', + Type: 'NS', + TTL: 172800, + ResourceRecords: [{ Value: 'one' }, { Value: 'two' }], + }, + }], + }, + }); +}); + +test('calls create resouce record set with DELETE for Delete event', async () => { + // GIVEN + mockStsClient.promise.mockResolvedValueOnce({ Credentials: { AccessKeyId: 'K', SecretAccessKey: 'S', SessionToken: 'T' } }); + mockRoute53Client.promise.mockResolvedValueOnce({}); + + // WHEN + const event= getCfnEvent({ RequestType: 'Delete' }); + await invokeHandler(event); + + // THEN + expect(mockRoute53Client.changeResourceRecordSets).toHaveBeenCalledTimes(1); + expect(mockRoute53Client.changeResourceRecordSets).toHaveBeenCalledWith({ + HostedZoneId: '1', + ChangeBatch: { + Changes: [{ + Action: 'DELETE', + ResourceRecordSet: { + Name: 'recordName', + Type: 'NS', + TTL: 172800, + ResourceRecords: [{ Value: 'one' }, { Value: 'two' }], + }, + }], + }, + }); +}); + +function getCfnEvent(event?: Partial): Partial { + return { + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'Foo', + AssumeRoleArn: 'roleArn', + ParentZoneId: '1', + DelegatedZoneName: 'recordName', + DelegatedZoneNameServers: ['one', 'two'], + TTL: 172800, + }, + ...event, + }; +} + +// helper function to get around TypeScript expecting a complete event object, +// even though our tests only need some of the fields +async function invokeHandler(event: Partial) { + return handler(event as AWSLambda.CloudFormationCustomResourceEvent); +} diff --git a/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts b/packages/@aws-cdk/aws-route53/test/hosted-zone-provider.test.ts similarity index 97% rename from packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts rename to packages/@aws-cdk/aws-route53/test/hosted-zone-provider.test.ts index 98778fe9d5107..07917bb83ba40 100644 --- a/packages/@aws-cdk/aws-route53/test/test.hosted-zone-provider.ts +++ b/packages/@aws-cdk/aws-route53/test/hosted-zone-provider.test.ts @@ -1,9 +1,9 @@ import { SynthUtils } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import { HostedZone } from '../lib'; -export = { +nodeunitShim({ 'Hosted Zone Provider': { 'HostedZoneProvider will return context values if available'(test: Test) { // GIVEN @@ -82,4 +82,4 @@ export = { test.done(); }, }, -}; +}); diff --git a/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts b/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts new file mode 100644 index 0000000000000..6e7810824c2bf --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts @@ -0,0 +1,149 @@ +import { expect } from '@aws-cdk/assert'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { nodeunitShim, Test } from 'nodeunit-shim'; +import { HostedZone, PublicHostedZone } from '../lib'; + +nodeunitShim({ + 'Hosted Zone': { + 'Hosted Zone constructs the ARN'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, 'TestStack', { + env: { account: '123456789012', region: 'us-east-1' }, + }); + + const testZone = new HostedZone(stack, 'HostedZone', { + zoneName: 'testZone', + }); + + test.deepEqual(stack.resolve(testZone.hostedZoneArn), { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':route53:::hostedzone/', + { Ref: 'HostedZoneDB99F866' }, + ], + ], + }); + + test.done(); + }, + }, + + 'Supports tags'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const hostedZone = new HostedZone(stack, 'HostedZone', { + zoneName: 'test.zone', + }); + cdk.Tags.of(hostedZone).add('zoneTag', 'inMyZone'); + + // THEN + expect(stack).toMatch({ + Resources: { + HostedZoneDB99F866: { + Type: 'AWS::Route53::HostedZone', + Properties: { + Name: 'test.zone.', + HostedZoneTags: [ + { + Key: 'zoneTag', + Value: 'inMyZone', + }, + ], + }, + }, + }, + }); + + test.done(); + }, + + 'with crossAccountZoneDelegationPrinciple'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, 'TestStack', { + env: { account: '123456789012', region: 'us-east-1' }, + }); + + // WHEN + new PublicHostedZone(stack, 'HostedZone', { + zoneName: 'testZone', + crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal('223456789012'), + }); + + // THEN + expect(stack).toMatch({ + Resources: { + HostedZoneDB99F866: { + Type: 'AWS::Route53::HostedZone', + Properties: { + Name: 'testZone.', + }, + }, + HostedZoneCrossAccountZoneDelegationRole685DF755: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::223456789012:root', + ], + ], + }, + }, + }, + ], + Version: '2012-10-17', + }, + Policies: [ + { + PolicyDocument: { + Statement: [ + { + Action: 'route53:ChangeResourceRecordSets', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':route53:::hostedzone/', + { + Ref: 'HostedZoneDB99F866', + }, + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'delegation', + }, + ], + }, + }, + }, + }); + + test.done(); + }, +}); diff --git a/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json new file mode 100644 index 0000000000000..919a54f8b5051 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.expected.json @@ -0,0 +1,219 @@ +{ + "Resources": { + "ParentHostedZoneC2BD86E1": { + "Type": "AWS::Route53::HostedZone", + "Properties": { + "Name": "myzone.com." + } + }, + "ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "route53:ChangeResourceRecordSets", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":route53:::hostedzone/", + { + "Ref": "ParentHostedZoneC2BD86E1" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "delegation" + } + ] + } + }, + "ChildHostedZone4B14AC71": { + "Type": "AWS::Route53::HostedZone", + "Properties": { + "Name": "sub.myzone.com." + } + }, + "DelegationCrossAccountZoneDelegationCustomResourceFADC27F0": { + "Type": "Custom::CrossAccountZoneDelegation", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265", + "Arn" + ] + }, + "AssumeRoleArn": { + "Fn::GetAtt": [ + "ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E", + "Arn" + ] + }, + "ParentZoneId": { + "Ref": "ParentHostedZoneC2BD86E1" + }, + "DelegatedZoneName": "sub.myzone.com", + "DelegatedZoneNameServers": { + "Fn::GetAtt": [ + "ChildHostedZone4B14AC71", + "NameServers" + ] + }, + "TTL": 172800 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomCrossAccountZoneDelegationCustomResourceProviderRoleED64687B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": { + "Fn::GetAtt": [ + "ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E", + "Arn" + ] + } + } + ] + } + } + ] + } + }, + "CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3Bucket8B462894" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3VersionKeyFDEC5E1D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3VersionKeyFDEC5E1D" + } + ] + } + ] + } + ] + ] + } + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "CustomCrossAccountZoneDelegationCustomResourceProviderRoleED64687B", + "Arn" + ] + }, + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "CustomCrossAccountZoneDelegationCustomResourceProviderRoleED64687B" + ] + } + }, + "Parameters": { + "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3Bucket8B462894": { + "Type": "String", + "Description": "S3 bucket for asset \"3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113a\"" + }, + "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aS3VersionKeyFDEC5E1D": { + "Type": "String", + "Description": "S3 key for asset version \"3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113a\"" + }, + "AssetParameters3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113aArtifactHash4F367D8C": { + "Type": "String", + "Description": "Artifact hash for asset \"3c971020239d152fff59dc3bdbabbbc6d3d9140574e45fd4eb7313ced117113a\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.ts b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.ts new file mode 100644 index 0000000000000..75f9e86152eb0 --- /dev/null +++ b/packages/@aws-cdk/aws-route53/test/integ.cross-account-zone-delegation.ts @@ -0,0 +1,23 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { PublicHostedZone, CrossAccountZoneDelegationRecord } from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-route53-cross-account-integ'); + +const parentZone = new PublicHostedZone(stack, 'ParentHostedZone', { + zoneName: 'myzone.com', + crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal(cdk.Aws.ACCOUNT_ID), +}); + +const childZone = new PublicHostedZone(stack, 'ChildHostedZone', { + zoneName: 'sub.myzone.com', +}); +new CrossAccountZoneDelegationRecord(stack, 'Delegation', { + delegatedZone: childZone, + parentHostedZoneId: parentZone.hostedZoneId, + delegationRole: parentZone.crossAccountZoneDelegationRole!, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-route53/test/test.record-set.ts b/packages/@aws-cdk/aws-route53/test/record-set.test.ts similarity index 88% rename from packages/@aws-cdk/aws-route53/test/test.record-set.ts rename to packages/@aws-cdk/aws-route53/test/record-set.test.ts index 8da38f60d9720..373464f455992 100644 --- a/packages/@aws-cdk/aws-route53/test/test.record-set.ts +++ b/packages/@aws-cdk/aws-route53/test/record-set.test.ts @@ -1,9 +1,10 @@ import { expect, haveResource } from '@aws-cdk/assert'; +import * as iam from '@aws-cdk/aws-iam'; import { Duration, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as route53 from '../lib'; -export = { +nodeunitShim({ 'with default ttl'(test: Test) { // GIVEN const stack = new Stack(); @@ -513,4 +514,52 @@ export = { })); test.done(); }, -}; + + 'Cross account zone delegation record'(test: Test) { + // GIVEN + const stack = new Stack(); + const parentZone = new route53.PublicHostedZone(stack, 'ParentHostedZone', { + zoneName: 'myzone.com', + crossAccountZoneDelegationPrincipal: new iam.AccountPrincipal('123456789012'), + }); + + // WHEN + const childZone = new route53.PublicHostedZone(stack, 'ChildHostedZone', { + zoneName: 'sub.myzone.com', + }); + new route53.CrossAccountZoneDelegationRecord(stack, 'Delegation', { + delegatedZone: childZone, + parentHostedZoneId: parentZone.hostedZoneId, + delegationRole: parentZone.crossAccountZoneDelegationRole!, + ttl: Duration.seconds(60), + }); + + // THEN + expect(stack).to(haveResource('Custom::CrossAccountZoneDelegation', { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265', + 'Arn', + ], + }, + AssumeRoleArn: { + 'Fn::GetAtt': [ + 'ParentHostedZoneCrossAccountZoneDelegationRole95B1C36E', + 'Arn', + ], + }, + ParentZoneId: { + Ref: 'ParentHostedZoneC2BD86E1', + }, + DelegatedZoneName: 'sub.myzone.com', + DelegatedZoneNameServers: { + 'Fn::GetAtt': [ + 'ChildHostedZone4B14AC71', + 'NameServers', + ], + }, + TTL: 60, + })); + test.done(); + }, +}); diff --git a/packages/@aws-cdk/aws-route53/test/test.route53.ts b/packages/@aws-cdk/aws-route53/test/route53.test.ts similarity index 98% rename from packages/@aws-cdk/aws-route53/test/test.route53.ts rename to packages/@aws-cdk/aws-route53/test/route53.test.ts index 4655e1c10fda8..8f58486bebcbc 100644 --- a/packages/@aws-cdk/aws-route53/test/test.route53.ts +++ b/packages/@aws-cdk/aws-route53/test/route53.test.ts @@ -1,10 +1,10 @@ import { beASupersetOfTemplate, exactlyMatchTemplate, expect, haveResource } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import { HostedZone, PrivateHostedZone, PublicHostedZone, TxtRecord } from '../lib'; -export = { +nodeunitShim({ 'default properties': { 'public hosted zone'(test: Test) { const app = new TestApp(); @@ -215,7 +215,7 @@ export = { })); test.done(); }, -}; +}); class TestApp { public readonly stack: cdk.Stack; diff --git a/packages/@aws-cdk/aws-route53/test/test.hosted-zone.ts b/packages/@aws-cdk/aws-route53/test/test.hosted-zone.ts deleted file mode 100644 index 37d80a5908a9b..0000000000000 --- a/packages/@aws-cdk/aws-route53/test/test.hosted-zone.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { expect } from '@aws-cdk/assert'; -import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import { HostedZone } from '../lib'; - -export = { - 'Hosted Zone': { - 'Hosted Zone constructs the ARN'(test: Test) { - // GIVEN - const stack = new cdk.Stack(undefined, 'TestStack', { - env: { account: '123456789012', region: 'us-east-1' }, - }); - - const testZone = new HostedZone(stack, 'HostedZone', { - zoneName: 'testZone', - }); - - test.deepEqual(stack.resolve(testZone.hostedZoneArn), { - 'Fn::Join': [ - '', - [ - 'arn:', - { Ref: 'AWS::Partition' }, - ':route53:::hostedzone/', - { Ref: 'HostedZoneDB99F866' }, - ], - ], - }); - - test.done(); - }, - }, - - 'Supports tags'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const hostedZone = new HostedZone(stack, 'HostedZone', { - zoneName: 'test.zone', - }); - cdk.Tags.of(hostedZone).add('zoneTag', 'inMyZone'); - - // THEN - expect(stack).toMatch({ - Resources: { - HostedZoneDB99F866: { - Type: 'AWS::Route53::HostedZone', - Properties: { - Name: 'test.zone.', - HostedZoneTags: [ - { - Key: 'zoneTag', - Value: 'inMyZone', - }, - ], - }, - }, - }, - }); - - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-route53/test/test.util.ts b/packages/@aws-cdk/aws-route53/test/util.test.ts similarity index 97% rename from packages/@aws-cdk/aws-route53/test/test.util.ts rename to packages/@aws-cdk/aws-route53/test/util.test.ts index d589b058e40cc..c6ded4e74f7b4 100644 --- a/packages/@aws-cdk/aws-route53/test/test.util.ts +++ b/packages/@aws-cdk/aws-route53/test/util.test.ts @@ -1,9 +1,9 @@ import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import { HostedZone } from '../lib'; import * as util from '../lib/util'; -export = { +nodeunitShim({ 'throws when zone name ending with a \'.\''(test: Test) { test.throws(() => util.validateZoneName('zone.name.'), /trailing dot/); test.done(); @@ -78,4 +78,4 @@ export = { test.equal(qualified, 'test.domain.com.'); test.done(); }, -}; +}); diff --git a/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts index 70ba07c201d5f..86edd9992776d 100644 --- a/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts +++ b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable jest/no-disabled-tests */ import { expect as cdkExpect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { IVpcEndpointServiceLoadBalancer, VpcEndpointService } from '@aws-cdk/aws-ec2'; @@ -56,7 +57,7 @@ test('create domain name resource', () => { }, }, physicalResourceId: { - id: 'EndpointDomain', + id: 'VPCES', }, }, Update: { @@ -69,7 +70,7 @@ test('create domain name resource', () => { }, }, physicalResourceId: { - id: 'EndpointDomain', + id: 'VPCES', }, }, Delete: { @@ -236,6 +237,9 @@ test('create domain name resource', () => { test('throws if creating multiple domains for a single service', () => { // GIVEN + vpces = new VpcEndpointService(stack, 'VPCES-2', { + vpcEndpointServiceLoadBalancers: [nlb], + }); new VpcEndpointServiceDomainName(stack, 'EndpointDomain', { endpointService: vpces, @@ -250,5 +254,5 @@ test('throws if creating multiple domains for a single service', () => { domainName: 'my-stuff-2.aws-cdk.dev', publicHostedZone: zone, }); - }).toThrow(); + }).toThrow(/Cannot create a VpcEndpointServiceDomainName for service/); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-assets/README.md b/packages/@aws-cdk/aws-s3-assets/README.md index 3d508070d6a50..aab4c46d9c44d 100644 --- a/packages/@aws-cdk/aws-s3-assets/README.md +++ b/packages/@aws-cdk/aws-s3-assets/README.md @@ -115,6 +115,7 @@ new assets.Asset(this, 'BundledAsset', { }, // Docker bundling fallback image: BundlingDockerImage.fromRegistry('alpine'), + entrypoint: ['/bin/sh', '-c'], command: ['bundle'], }, }); diff --git a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts index bbf526b79df86..4ca33864952f3 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts +++ b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts @@ -227,7 +227,7 @@ export class BucketDeployment extends CoreConstruct { Prune: props.prune ?? true, UserMetadata: props.metadata ? mapUserMetadata(props.metadata) : undefined, SystemMetadata: mapSystemMetadata(props), - DistributionId: props.distribution ? props.distribution.distributionId : undefined, + DistributionId: props.distribution?.distributionId, DistributionPaths: props.distributionPaths, }, }); diff --git a/packages/@aws-cdk/aws-s3-deployment/package.json b/packages/@aws-cdk/aws-s3-deployment/package.json index a035fdb80e343..cf9089e39d715 100644 --- a/packages/@aws-cdk/aws-s3-deployment/package.json +++ b/packages/@aws-cdk/aws-s3-deployment/package.json @@ -78,7 +78,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@aws-cdk/aws-s3-notifications/lib/lambda.ts b/packages/@aws-cdk/aws-s3-notifications/lib/lambda.ts index 36ad917ec4ec4..183512a996e26 100644 --- a/packages/@aws-cdk/aws-s3-notifications/lib/lambda.ts +++ b/packages/@aws-cdk/aws-s3-notifications/lib/lambda.ts @@ -1,7 +1,11 @@ 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, Names, Stack } from '@aws-cdk/core'; +import { CfnResource, Names, Stack } from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; /** * Use a Lambda function as a bucket notification destination diff --git a/packages/@aws-cdk/aws-s3-notifications/test/integ.notifications.expected.json b/packages/@aws-cdk/aws-s3-notifications/test/integ.notifications.expected.json index e28b703234d0d..4e12e4dd9badf 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/integ.notifications.expected.json +++ b/packages/@aws-cdk/aws-s3-notifications/test/integ.notifications.expected.json @@ -211,7 +211,7 @@ "Properties": { "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", "Code": { - "ZipFile": "exports.handler = (event, context) => {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies\n const s3 = new (require('aws-sdk').S3)();\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const https = require('https');\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const url = require('url');\n log(JSON.stringify(event, undefined, 2));\n const props = event.ResourceProperties;\n if (event.RequestType === 'Delete') {\n props.NotificationConfiguration = {}; // this is how you clean out notifications\n }\n const req = {\n Bucket: props.BucketName,\n NotificationConfiguration: props.NotificationConfiguration,\n };\n return s3.putBucketNotificationConfiguration(req, (err, data) => {\n log({ err, data });\n if (err) {\n return submitResponse('FAILED', err.message + `\\nMore information in CloudWatch Log Stream: ${context.logStreamName}`);\n }\n else {\n return submitResponse('SUCCESS');\n }\n });\n function log(obj) {\n console.error(event.RequestId, event.StackId, event.LogicalResourceId, obj);\n }\n // eslint-disable-next-line max-len\n // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule\n // to allow sending an error messge as a reason.\n function submitResponse(responseStatus, reason) {\n const responseBody = JSON.stringify({\n Status: responseStatus,\n Reason: reason || 'See the details in CloudWatch Log Stream: ' + context.logStreamName,\n PhysicalResourceId: event.PhysicalResourceId || event.LogicalResourceId,\n StackId: event.StackId,\n RequestId: event.RequestId,\n LogicalResourceId: event.LogicalResourceId,\n NoEcho: false,\n });\n log({ responseBody });\n const parsedUrl = url.parse(event.ResponseURL);\n const options = {\n hostname: parsedUrl.hostname,\n port: 443,\n path: parsedUrl.path,\n method: 'PUT',\n headers: {\n 'content-type': '',\n 'content-length': responseBody.length,\n },\n };\n const request = https.request(options, (r) => {\n log({ statusCode: r.statusCode, statusMessage: r.statusMessage });\n context.done();\n });\n request.on('error', (error) => {\n log({ sendError: error });\n context.done();\n });\n request.write(responseBody);\n request.end();\n }\n};" + "ZipFile": "exports.handler = (event, context) => {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies\n const s3 = new (require('aws-sdk').S3)();\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const https = require('https');\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const url = require('url');\n log(JSON.stringify(event, undefined, 2));\n const props = event.ResourceProperties;\n if (event.RequestType === 'Delete') {\n props.NotificationConfiguration = {}; // this is how you clean out notifications\n }\n const req = {\n Bucket: props.BucketName,\n NotificationConfiguration: props.NotificationConfiguration,\n };\n return s3.putBucketNotificationConfiguration(req, (err, data) => {\n log({ err, data });\n if (err) {\n return submitResponse('FAILED', err.message + `\\nMore information in CloudWatch Log Stream: ${context.logStreamName}`);\n }\n else {\n return submitResponse('SUCCESS');\n }\n });\n function log(obj) {\n console.error(event.RequestId, event.StackId, event.LogicalResourceId, obj);\n }\n // eslint-disable-next-line max-len\n // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule\n // to allow sending an error message as a reason.\n function submitResponse(responseStatus, reason) {\n const responseBody = JSON.stringify({\n Status: responseStatus,\n Reason: reason || 'See the details in CloudWatch Log Stream: ' + context.logStreamName,\n PhysicalResourceId: event.PhysicalResourceId || event.LogicalResourceId,\n StackId: event.StackId,\n RequestId: event.RequestId,\n LogicalResourceId: event.LogicalResourceId,\n NoEcho: false,\n });\n log({ responseBody });\n const parsedUrl = url.parse(event.ResponseURL);\n const options = {\n hostname: parsedUrl.hostname,\n port: 443,\n path: parsedUrl.path,\n method: 'PUT',\n headers: {\n 'content-type': '',\n 'content-length': responseBody.length,\n },\n };\n const request = https.request(options, (r) => {\n log({ statusCode: r.statusCode, statusMessage: r.statusMessage });\n context.done();\n });\n request.on('error', (error) => {\n log({ sendError: error });\n context.done();\n });\n request.write(responseBody);\n request.end();\n }\n};" }, "Handler": "index.handler", "Role": { diff --git a/packages/@aws-cdk/aws-s3-notifications/test/lambda/integ.bucket-notifications.expected.json b/packages/@aws-cdk/aws-s3-notifications/test/lambda/integ.bucket-notifications.expected.json index 98ddf87498738..8a1c134ff2651 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/lambda/integ.bucket-notifications.expected.json +++ b/packages/@aws-cdk/aws-s3-notifications/test/lambda/integ.bucket-notifications.expected.json @@ -235,7 +235,7 @@ "Properties": { "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", "Code": { - "ZipFile": "exports.handler = (event, context) => {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies\n const s3 = new (require('aws-sdk').S3)();\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const https = require('https');\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const url = require('url');\n log(JSON.stringify(event, undefined, 2));\n const props = event.ResourceProperties;\n if (event.RequestType === 'Delete') {\n props.NotificationConfiguration = {}; // this is how you clean out notifications\n }\n const req = {\n Bucket: props.BucketName,\n NotificationConfiguration: props.NotificationConfiguration,\n };\n return s3.putBucketNotificationConfiguration(req, (err, data) => {\n log({ err, data });\n if (err) {\n return submitResponse('FAILED', err.message + `\\nMore information in CloudWatch Log Stream: ${context.logStreamName}`);\n }\n else {\n return submitResponse('SUCCESS');\n }\n });\n function log(obj) {\n console.error(event.RequestId, event.StackId, event.LogicalResourceId, obj);\n }\n // eslint-disable-next-line max-len\n // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule\n // to allow sending an error messge as a reason.\n function submitResponse(responseStatus, reason) {\n const responseBody = JSON.stringify({\n Status: responseStatus,\n Reason: reason || 'See the details in CloudWatch Log Stream: ' + context.logStreamName,\n PhysicalResourceId: event.PhysicalResourceId || event.LogicalResourceId,\n StackId: event.StackId,\n RequestId: event.RequestId,\n LogicalResourceId: event.LogicalResourceId,\n NoEcho: false,\n });\n log({ responseBody });\n const parsedUrl = url.parse(event.ResponseURL);\n const options = {\n hostname: parsedUrl.hostname,\n port: 443,\n path: parsedUrl.path,\n method: 'PUT',\n headers: {\n 'content-type': '',\n 'content-length': responseBody.length,\n },\n };\n const request = https.request(options, (r) => {\n log({ statusCode: r.statusCode, statusMessage: r.statusMessage });\n context.done();\n });\n request.on('error', (error) => {\n log({ sendError: error });\n context.done();\n });\n request.write(responseBody);\n request.end();\n }\n};" + "ZipFile": "exports.handler = (event, context) => {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies\n const s3 = new (require('aws-sdk').S3)();\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const https = require('https');\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const url = require('url');\n log(JSON.stringify(event, undefined, 2));\n const props = event.ResourceProperties;\n if (event.RequestType === 'Delete') {\n props.NotificationConfiguration = {}; // this is how you clean out notifications\n }\n const req = {\n Bucket: props.BucketName,\n NotificationConfiguration: props.NotificationConfiguration,\n };\n return s3.putBucketNotificationConfiguration(req, (err, data) => {\n log({ err, data });\n if (err) {\n return submitResponse('FAILED', err.message + `\\nMore information in CloudWatch Log Stream: ${context.logStreamName}`);\n }\n else {\n return submitResponse('SUCCESS');\n }\n });\n function log(obj) {\n console.error(event.RequestId, event.StackId, event.LogicalResourceId, obj);\n }\n // eslint-disable-next-line max-len\n // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule\n // to allow sending an error message as a reason.\n function submitResponse(responseStatus, reason) {\n const responseBody = JSON.stringify({\n Status: responseStatus,\n Reason: reason || 'See the details in CloudWatch Log Stream: ' + context.logStreamName,\n PhysicalResourceId: event.PhysicalResourceId || event.LogicalResourceId,\n StackId: event.StackId,\n RequestId: event.RequestId,\n LogicalResourceId: event.LogicalResourceId,\n NoEcho: false,\n });\n log({ responseBody });\n const parsedUrl = url.parse(event.ResponseURL);\n const options = {\n hostname: parsedUrl.hostname,\n port: 443,\n path: parsedUrl.path,\n method: 'PUT',\n headers: {\n 'content-type': '',\n 'content-length': responseBody.length,\n },\n };\n const request = https.request(options, (r) => {\n log({ statusCode: r.statusCode, statusMessage: r.statusMessage });\n context.done();\n });\n request.on('error', (error) => {\n log({ sendError: error });\n context.done();\n });\n request.write(responseBody);\n request.end();\n }\n};" }, "Handler": "index.handler", "Role": { diff --git a/packages/@aws-cdk/aws-s3-notifications/test/queue.test.ts b/packages/@aws-cdk/aws-s3-notifications/test/queue.test.ts index 5e384d2377318..9cc4dad9712c7 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/queue.test.ts +++ b/packages/@aws-cdk/aws-s3-notifications/test/queue.test.ts @@ -1,4 +1,4 @@ -import { SynthUtils } from '@aws-cdk/assert'; +import { arrayWith, SynthUtils } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as s3 from '@aws-cdk/aws-s3'; import * as sqs from '@aws-cdk/aws-sqs'; @@ -77,65 +77,19 @@ test('if the queue is encrypted with a custom kms key, the key resource policy i bucket.addObjectCreatedNotification(new notif.SqsDestination(queue)); - expect(stack).toHaveResource('AWS::KMS::Key', { + expect(stack).toHaveResourceLike('AWS::KMS::Key', { KeyPolicy: { - Statement: [ - { - Action: [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', - ], - Effect: 'Allow', - Principal: { - AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':root']] }, - }, - Resource: '*', - }, - { - Action: [ - 'kms:Decrypt', - 'kms:Encrypt', - 'kms:ReEncrypt*', - 'kms:GenerateDataKey*', - ], - Condition: { - ArnLike: { - 'aws:SourceArn': { 'Fn::GetAtt': ['Bucket83908E77', 'Arn'] }, - }, - }, - Effect: 'Allow', - Principal: { - Service: 's3.amazonaws.com', - }, - Resource: '*', + Statement: arrayWith({ + Action: [ + 'kms:GenerateDataKey*', + 'kms:Decrypt', + ], + Effect: 'Allow', + Principal: { + Service: 's3.amazonaws.com', }, - { - Action: [ - 'kms:GenerateDataKey*', - 'kms:Decrypt', - ], - Effect: 'Allow', - Principal: { - Service: 's3.amazonaws.com', - }, - Resource: '*', - }, - ], - Version: '2012-10-17', + Resource: '*', + }), }, - Description: 'Created by Default/Queue', }); }); diff --git a/packages/@aws-cdk/aws-s3-notifications/test/sns/integ.sns-bucket-notifications.expected.json b/packages/@aws-cdk/aws-s3-notifications/test/sns/integ.sns-bucket-notifications.expected.json index 3ee1f5979fbdf..0aabfdd9f8e19 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/sns/integ.sns-bucket-notifications.expected.json +++ b/packages/@aws-cdk/aws-s3-notifications/test/sns/integ.sns-bucket-notifications.expected.json @@ -194,7 +194,7 @@ "Properties": { "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", "Code": { - "ZipFile": "exports.handler = (event, context) => {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies\n const s3 = new (require('aws-sdk').S3)();\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const https = require('https');\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const url = require('url');\n log(JSON.stringify(event, undefined, 2));\n const props = event.ResourceProperties;\n if (event.RequestType === 'Delete') {\n props.NotificationConfiguration = {}; // this is how you clean out notifications\n }\n const req = {\n Bucket: props.BucketName,\n NotificationConfiguration: props.NotificationConfiguration,\n };\n return s3.putBucketNotificationConfiguration(req, (err, data) => {\n log({ err, data });\n if (err) {\n return submitResponse('FAILED', err.message + `\\nMore information in CloudWatch Log Stream: ${context.logStreamName}`);\n }\n else {\n return submitResponse('SUCCESS');\n }\n });\n function log(obj) {\n console.error(event.RequestId, event.StackId, event.LogicalResourceId, obj);\n }\n // eslint-disable-next-line max-len\n // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule\n // to allow sending an error messge as a reason.\n function submitResponse(responseStatus, reason) {\n const responseBody = JSON.stringify({\n Status: responseStatus,\n Reason: reason || 'See the details in CloudWatch Log Stream: ' + context.logStreamName,\n PhysicalResourceId: event.PhysicalResourceId || event.LogicalResourceId,\n StackId: event.StackId,\n RequestId: event.RequestId,\n LogicalResourceId: event.LogicalResourceId,\n NoEcho: false,\n });\n log({ responseBody });\n const parsedUrl = url.parse(event.ResponseURL);\n const options = {\n hostname: parsedUrl.hostname,\n port: 443,\n path: parsedUrl.path,\n method: 'PUT',\n headers: {\n 'content-type': '',\n 'content-length': responseBody.length,\n },\n };\n const request = https.request(options, (r) => {\n log({ statusCode: r.statusCode, statusMessage: r.statusMessage });\n context.done();\n });\n request.on('error', (error) => {\n log({ sendError: error });\n context.done();\n });\n request.write(responseBody);\n request.end();\n }\n};" + "ZipFile": "exports.handler = (event, context) => {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies\n const s3 = new (require('aws-sdk').S3)();\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const https = require('https');\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const url = require('url');\n log(JSON.stringify(event, undefined, 2));\n const props = event.ResourceProperties;\n if (event.RequestType === 'Delete') {\n props.NotificationConfiguration = {}; // this is how you clean out notifications\n }\n const req = {\n Bucket: props.BucketName,\n NotificationConfiguration: props.NotificationConfiguration,\n };\n return s3.putBucketNotificationConfiguration(req, (err, data) => {\n log({ err, data });\n if (err) {\n return submitResponse('FAILED', err.message + `\\nMore information in CloudWatch Log Stream: ${context.logStreamName}`);\n }\n else {\n return submitResponse('SUCCESS');\n }\n });\n function log(obj) {\n console.error(event.RequestId, event.StackId, event.LogicalResourceId, obj);\n }\n // eslint-disable-next-line max-len\n // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule\n // to allow sending an error message as a reason.\n function submitResponse(responseStatus, reason) {\n const responseBody = JSON.stringify({\n Status: responseStatus,\n Reason: reason || 'See the details in CloudWatch Log Stream: ' + context.logStreamName,\n PhysicalResourceId: event.PhysicalResourceId || event.LogicalResourceId,\n StackId: event.StackId,\n RequestId: event.RequestId,\n LogicalResourceId: event.LogicalResourceId,\n NoEcho: false,\n });\n log({ responseBody });\n const parsedUrl = url.parse(event.ResponseURL);\n const options = {\n hostname: parsedUrl.hostname,\n port: 443,\n path: parsedUrl.path,\n method: 'PUT',\n headers: {\n 'content-type': '',\n 'content-length': responseBody.length,\n },\n };\n const request = https.request(options, (r) => {\n log({ statusCode: r.statusCode, statusMessage: r.statusMessage });\n context.done();\n });\n request.on('error', (error) => {\n log({ sendError: error });\n context.done();\n });\n request.write(responseBody);\n request.end();\n }\n};" }, "Handler": "index.handler", "Role": { diff --git a/packages/@aws-cdk/aws-s3-notifications/test/sqs/integ.bucket-notifications.expected.json b/packages/@aws-cdk/aws-s3-notifications/test/sqs/integ.bucket-notifications.expected.json index b4d969a3c6d42..afcc652c50f99 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/sqs/integ.bucket-notifications.expected.json +++ b/packages/@aws-cdk/aws-s3-notifications/test/sqs/integ.bucket-notifications.expected.json @@ -181,7 +181,7 @@ "Properties": { "Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)", "Code": { - "ZipFile": "exports.handler = (event, context) => {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies\n const s3 = new (require('aws-sdk').S3)();\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const https = require('https');\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const url = require('url');\n log(JSON.stringify(event, undefined, 2));\n const props = event.ResourceProperties;\n if (event.RequestType === 'Delete') {\n props.NotificationConfiguration = {}; // this is how you clean out notifications\n }\n const req = {\n Bucket: props.BucketName,\n NotificationConfiguration: props.NotificationConfiguration,\n };\n return s3.putBucketNotificationConfiguration(req, (err, data) => {\n log({ err, data });\n if (err) {\n return submitResponse('FAILED', err.message + `\\nMore information in CloudWatch Log Stream: ${context.logStreamName}`);\n }\n else {\n return submitResponse('SUCCESS');\n }\n });\n function log(obj) {\n console.error(event.RequestId, event.StackId, event.LogicalResourceId, obj);\n }\n // eslint-disable-next-line max-len\n // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule\n // to allow sending an error messge as a reason.\n function submitResponse(responseStatus, reason) {\n const responseBody = JSON.stringify({\n Status: responseStatus,\n Reason: reason || 'See the details in CloudWatch Log Stream: ' + context.logStreamName,\n PhysicalResourceId: event.PhysicalResourceId || event.LogicalResourceId,\n StackId: event.StackId,\n RequestId: event.RequestId,\n LogicalResourceId: event.LogicalResourceId,\n NoEcho: false,\n });\n log({ responseBody });\n const parsedUrl = url.parse(event.ResponseURL);\n const options = {\n hostname: parsedUrl.hostname,\n port: 443,\n path: parsedUrl.path,\n method: 'PUT',\n headers: {\n 'content-type': '',\n 'content-length': responseBody.length,\n },\n };\n const request = https.request(options, (r) => {\n log({ statusCode: r.statusCode, statusMessage: r.statusMessage });\n context.done();\n });\n request.on('error', (error) => {\n log({ sendError: error });\n context.done();\n });\n request.write(responseBody);\n request.end();\n }\n};" + "ZipFile": "exports.handler = (event, context) => {\n // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies\n const s3 = new (require('aws-sdk').S3)();\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const https = require('https');\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const url = require('url');\n log(JSON.stringify(event, undefined, 2));\n const props = event.ResourceProperties;\n if (event.RequestType === 'Delete') {\n props.NotificationConfiguration = {}; // this is how you clean out notifications\n }\n const req = {\n Bucket: props.BucketName,\n NotificationConfiguration: props.NotificationConfiguration,\n };\n return s3.putBucketNotificationConfiguration(req, (err, data) => {\n log({ err, data });\n if (err) {\n return submitResponse('FAILED', err.message + `\\nMore information in CloudWatch Log Stream: ${context.logStreamName}`);\n }\n else {\n return submitResponse('SUCCESS');\n }\n });\n function log(obj) {\n console.error(event.RequestId, event.StackId, event.LogicalResourceId, obj);\n }\n // eslint-disable-next-line max-len\n // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule\n // to allow sending an error message as a reason.\n function submitResponse(responseStatus, reason) {\n const responseBody = JSON.stringify({\n Status: responseStatus,\n Reason: reason || 'See the details in CloudWatch Log Stream: ' + context.logStreamName,\n PhysicalResourceId: event.PhysicalResourceId || event.LogicalResourceId,\n StackId: event.StackId,\n RequestId: event.RequestId,\n LogicalResourceId: event.LogicalResourceId,\n NoEcho: false,\n });\n log({ responseBody });\n const parsedUrl = url.parse(event.ResponseURL);\n const options = {\n hostname: parsedUrl.hostname,\n port: 443,\n path: parsedUrl.path,\n method: 'PUT',\n headers: {\n 'content-type': '',\n 'content-length': responseBody.length,\n },\n };\n const request = https.request(options, (r) => {\n log({ statusCode: r.statusCode, statusMessage: r.statusMessage });\n context.done();\n });\n request.on('error', (error) => {\n log({ sendError: error });\n context.done();\n });\n request.write(responseBody);\n request.end();\n }\n};" }, "Handler": "index.handler", "Role": { diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index 1ce00f56ba0a9..12ae88bb02d2e 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -67,8 +67,6 @@ assert(bucket.encryptionKey === myKmsKey); Enable KMS-SSE encryption via [S3 Bucket Keys](https://docs.aws.amazon.com/AmazonS3/latest/dev/bucket-key.html): ```ts -const myKmsKey = new kms.Key(this, 'MyKey'); - const bucket = new Bucket(this, 'MyEncryptedBucket', { encryption: BucketEncryption.KMS, bucketKeyEnabled: true diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 5c5abfbfec559..859f774565a93 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -147,6 +147,14 @@ export interface IBucket extends IResource { * If encryption is used, permission to use the key to encrypt the contents * of written files will also be granted to the same principal. * + * Before CDK version 1.85.0, this method granted the `s3:PutObject*` permission that included `s3:PutObjectAcl`, + * which could be used to grant read/write object access to IAM principals in other accounts. + * If you want to get rid of that behavior, update your CDK version to 1.85.0 or later, + * and make sure the `@aws-cdk/aws-s3:grantWriteWithoutAcl` feature flag is set to `true` + * in the `context` key of your cdk.json file. + * If you've already updated, but still need the principal to have permissions to modify the ACLs, + * use the {@link grantPutAcl} method. + * * @param identity The principal * @param objectsKeyPattern Restrict the permission to a certain key pattern (default '*') */ @@ -190,6 +198,14 @@ export interface IBucket extends IResource { * If an encryption key is used, permission to use the key for * encrypt/decrypt will also be granted. * + * Before CDK version 1.85.0, this method granted the `s3:PutObject*` permission that included `s3:PutObjectAcl`, + * which could be used to grant read/write object access to IAM principals in other accounts. + * If you want to get rid of that behavior, update your CDK version to 1.85.0 or later, + * and make sure the `@aws-cdk/aws-s3:grantWriteWithoutAcl` feature flag is set to `true` + * in the `context` key of your cdk.json file. + * If you've already updated, but still need the principal to have permissions to modify the ACLs, + * use the {@link grantPutAcl} method. + * * @param identity The principal * @param objectsKeyPattern Restrict the permission to a certain key pattern (default '*') */ @@ -413,7 +429,7 @@ abstract class BucketBase extends Resource implements IBucket { detailType: ['AWS API Call via CloudTrail'], detail: { resources: { - ARN: options.paths ? options.paths.map(p => this.arnForObjects(p)) : [this.bucketArn], + ARN: options.paths?.map(p => this.arnForObjects(p)) ?? [this.bucketArn], }, }, }); @@ -587,15 +603,6 @@ abstract class BucketBase extends Resource implements IBucket { this.arnForObjects(objectsKeyPattern)); } - /** - * Grant write permissions to this bucket to an IAM principal. - * - * If encryption is used, permission to use the key to encrypt the contents - * of written files will also be granted to the same principal. - * - * @param identity The principal - * @param objectsKeyPattern Restrict the permission to a certain key pattern (default '*') - */ public grantWrite(identity: iam.IGrantable, objectsKeyPattern: any = '*') { return this.grant(identity, this.writeActions, perms.KEY_WRITE_ACTIONS, this.bucketArn, @@ -632,16 +639,6 @@ abstract class BucketBase extends Resource implements IBucket { this.arnForObjects(objectsKeyPattern)); } - /** - * Grants read/write permissions for this bucket and it's contents to an IAM - * principal (Role/Group/User). - * - * If an encryption key is used, permission to use the key for - * encrypt/decrypt will also be granted. - * - * @param identity The principal - * @param objectsKeyPattern Restrict the permission to a certain key pattern (default '*') - */ public grantReadWrite(identity: iam.IGrantable, objectsKeyPattern: any = '*') { const bucketActions = perms.BUCKET_READ_ACTIONS.concat(this.writeActions); // we need unique permissions because some permissions are common between read and write key actions @@ -1607,13 +1604,13 @@ export class Bucket extends BucketBase { return { rules: this.lifecycleRules.map(parseLifecycleRule) }; function parseLifecycleRule(rule: LifecycleRule): CfnBucket.RuleProperty { - const enabled = rule.enabled !== undefined ? rule.enabled : true; + const enabled = rule.enabled ?? true; const x: CfnBucket.RuleProperty = { // eslint-disable-next-line max-len abortIncompleteMultipartUpload: rule.abortIncompleteMultipartUploadAfter !== undefined ? { daysAfterInitiation: rule.abortIncompleteMultipartUploadAfter.toDays() } : undefined, expirationDate: rule.expirationDate, - expirationInDays: rule.expiration && rule.expiration.toDays(), + expirationInDays: rule.expiration?.toDays(), id: rule.id, noncurrentVersionExpirationInDays: rule.noncurrentVersionExpiration && rule.noncurrentVersionExpiration.toDays(), noncurrentVersionTransitions: mapOrUndefined(rule.noncurrentVersionTransitions, t => ({ diff --git a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts index cd7427a5cbc6f..3f07a0d5b7bdf 100644 --- a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts +++ b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts @@ -138,7 +138,7 @@ const handler = (event: any, context: any) => { // eslint-disable-next-line max-len // adapted from https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule - // to allow sending an error messge as a reason. + // to allow sending an error message as a reason. function submitResponse(responseStatus: string, reason?: string) { const responseBody = JSON.stringify({ Status: responseStatus, diff --git a/packages/@aws-cdk/aws-s3/test/bucket.test.ts b/packages/@aws-cdk/aws-s3/test/bucket.test.ts index 0d4e5b03f8258..51b2874311af8 100644 --- a/packages/@aws-cdk/aws-s3/test/bucket.test.ts +++ b/packages/@aws-cdk/aws-s3/test/bucket.test.ts @@ -1,22 +1,23 @@ +import '@aws-cdk/assert/jest'; import { EOL } from 'os'; -import { countResources, expect, haveResource, haveResourceLike, ResourcePart, SynthUtils, arrayWith, objectLike } from '@aws-cdk/assert'; +import { ResourcePart, SynthUtils, arrayWith, objectLike } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { nodeunitShim, Test } from 'nodeunit-shim'; +import { testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as s3 from '../lib'; // to make it easy to copy & paste from output: /* eslint-disable quote-props */ -nodeunitShim({ - 'default bucket'(test: Test) { +describe('bucket', () => { + test('default bucket', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket'); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -26,29 +27,29 @@ nodeunitShim({ }, }); - test.done(); - }, - 'CFN properties are type-validated during resolution'(test: Test) { + }); + + test('CFN properties are type-validated during resolution', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { bucketName: cdk.Token.asString(5), // Oh no }); - test.throws(() => { + expect(() => { SynthUtils.synthesize(stack); - }, /bucketName: 5 should be a string/); + }).toThrow(/bucketName: 5 should be a string/); + - test.done(); - }, + }); - 'bucket without encryption'(test: Test) { + test('bucket without encryption', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.UNENCRYPTED, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -58,16 +59,16 @@ nodeunitShim({ }, }); - test.done(); - }, - 'bucket with managed encryption'(test: Test) { + }); + + test('bucket with managed encryption', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS_MANAGED, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -87,34 +88,34 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'valid bucket names'(test: Test) { + }); + + test('valid bucket names', () => { const stack = new cdk.Stack(); - test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket1', { + expect(() => new s3.Bucket(stack, 'MyBucket1', { bucketName: 'abc.xyz-34ab', - })); + })).not.toThrow(); - test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket2', { + expect(() => new s3.Bucket(stack, 'MyBucket2', { bucketName: '124.pp--33', - })); + })).not.toThrow(); - test.done(); - }, - 'bucket validation skips tokenized values'(test: Test) { + }); + + test('bucket validation skips tokenized values', () => { const stack = new cdk.Stack(); - test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket', { + expect(() => new s3.Bucket(stack, 'MyBucket', { bucketName: cdk.Lazy.string({ produce: () => '_BUCKET' }), - })); + })).not.toThrow(); + - test.done(); - }, + }); - 'fails with message on invalid bucket names'(test: Test) { + test('fails with message on invalid bucket names', () => { const stack = new cdk.Stack(); const bucket = `-buckEt.-${new Array(65).join('$')}`; const expectedErrors = [ @@ -126,221 +127,162 @@ nodeunitShim({ 'Bucket name must not have dash next to period, or period next to dash, or consecutive periods (offset: 7)', ].join(EOL); - test.throws(() => new s3.Bucket(stack, 'MyBucket', { + expect(() => new s3.Bucket(stack, 'MyBucket', { bucketName: bucket, - }), expectedErrors); + })).toThrow(expectedErrors); + - test.done(); - }, + }); - 'fails if bucket name has less than 3 or more than 63 characters'(test: Test) { + test('fails if bucket name has less than 3 or more than 63 characters', () => { const stack = new cdk.Stack(); - test.throws(() => new s3.Bucket(stack, 'MyBucket1', { + expect(() => new s3.Bucket(stack, 'MyBucket1', { bucketName: 'a', - }), /at least 3/); + })).toThrow(/at least 3/); - test.throws(() => new s3.Bucket(stack, 'MyBucket2', { + expect(() => new s3.Bucket(stack, 'MyBucket2', { bucketName: new Array(65).join('x'), - }), /no more than 63/); + })).toThrow(/no more than 63/); - test.done(); - }, - 'fails if bucket name has invalid characters'(test: Test) { + }); + + test('fails if bucket name has invalid characters', () => { const stack = new cdk.Stack(); - test.throws(() => new s3.Bucket(stack, 'MyBucket1', { + expect(() => new s3.Bucket(stack, 'MyBucket1', { bucketName: 'b@cket', - }), /offset: 1/); + })).toThrow(/offset: 1/); - test.throws(() => new s3.Bucket(stack, 'MyBucket2', { + expect(() => new s3.Bucket(stack, 'MyBucket2', { bucketName: 'bucKet', - }), /offset: 3/); + })).toThrow(/offset: 3/); - test.throws(() => new s3.Bucket(stack, 'MyBucket3', { + expect(() => new s3.Bucket(stack, 'MyBucket3', { bucketName: 'bučket', - }), /offset: 2/); + })).toThrow(/offset: 2/); + - test.done(); - }, + }); - 'fails if bucket name does not start or end with lowercase character or number'(test: Test) { + test('fails if bucket name does not start or end with lowercase character or number', () => { const stack = new cdk.Stack(); - test.throws(() => new s3.Bucket(stack, 'MyBucket1', { + expect(() => new s3.Bucket(stack, 'MyBucket1', { bucketName: '-ucket', - }), /offset: 0/); + })).toThrow(/offset: 0/); - test.throws(() => new s3.Bucket(stack, 'MyBucket2', { + expect(() => new s3.Bucket(stack, 'MyBucket2', { bucketName: 'bucke.', - }), /offset: 5/); + })).toThrow(/offset: 5/); - test.done(); - }, - 'fails only if bucket name has the consecutive symbols (..), (.-), (-.)'(test: Test) { + }); + + test('fails only if bucket name has the consecutive symbols (..), (.-), (-.)', () => { const stack = new cdk.Stack(); - test.throws(() => new s3.Bucket(stack, 'MyBucket1', { + expect(() => new s3.Bucket(stack, 'MyBucket1', { bucketName: 'buc..ket', - }), /offset: 3/); + })).toThrow(/offset: 3/); - test.throws(() => new s3.Bucket(stack, 'MyBucket2', { + expect(() => new s3.Bucket(stack, 'MyBucket2', { bucketName: 'buck.-et', - }), /offset: 4/); + })).toThrow(/offset: 4/); - test.throws(() => new s3.Bucket(stack, 'MyBucket3', { + expect(() => new s3.Bucket(stack, 'MyBucket3', { bucketName: 'b-.ucket', - }), /offset: 1/); + })).toThrow(/offset: 1/); - test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket4', { + expect(() => new s3.Bucket(stack, 'MyBucket4', { bucketName: 'bu--cket', - })); + })).not.toThrow(); + - test.done(); - }, + }); - 'fails only if bucket name resembles IP address'(test: Test) { + test('fails only if bucket name resembles IP address', () => { const stack = new cdk.Stack(); - test.throws(() => new s3.Bucket(stack, 'MyBucket1', { + expect(() => new s3.Bucket(stack, 'MyBucket1', { bucketName: '1.2.3.4', - }), /must not resemble an IP address/); + })).toThrow(/must not resemble an IP address/); - test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket2', { + expect(() => new s3.Bucket(stack, 'MyBucket2', { bucketName: '1.2.3', - })); + })).not.toThrow(); - test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket3', { + expect(() => new s3.Bucket(stack, 'MyBucket3', { bucketName: '1.2.3.a', - })); + })).not.toThrow(); - test.doesNotThrow(() => new s3.Bucket(stack, 'MyBucket4', { + expect(() => new s3.Bucket(stack, 'MyBucket4', { bucketName: '1000.2.3.4', - })); + })).not.toThrow(); - test.done(); - }, - 'fails if encryption key is used with managed encryption'(test: Test) { + }); + + test('fails if encryption key is used with managed encryption', () => { const stack = new cdk.Stack(); const myKey = new kms.Key(stack, 'MyKey'); - test.throws(() => new s3.Bucket(stack, 'MyBucket', { + expect(() => new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS_MANAGED, encryptionKey: myKey, - }), /encryptionKey is specified, so 'encryption' must be set to KMS/); + })).toThrow(/encryptionKey is specified, so 'encryption' must be set to KMS/); + - test.done(); - }, + }); - 'fails if encryption key is used with encryption set to unencrypted'(test: Test) { + test('fails if encryption key is used with encryption set to unencrypted', () => { const stack = new cdk.Stack(); const myKey = new kms.Key(stack, 'MyKey'); - test.throws(() => new s3.Bucket(stack, 'MyBucket', { + expect(() => new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.UNENCRYPTED, encryptionKey: myKey, - }), /encryptionKey is specified, so 'encryption' must be set to KMS/); + })).toThrow(/encryptionKey is specified, so 'encryption' must be set to KMS/); + - test.done(); - }, + }); - 'encryptionKey can specify kms key'(test: Test) { + test('encryptionKey can specify kms key', () => { const stack = new cdk.Stack(); const encryptionKey = new kms.Key(stack, 'MyKey', { description: 'hello, world' }); new s3.Bucket(stack, 'MyBucket', { encryptionKey, encryption: s3.BucketEncryption.KMS }); - expect(stack).toMatch({ - 'Resources': { - 'MyKey6AB29FA6': { - 'Type': 'AWS::KMS::Key', - 'Properties': { - 'Description': 'hello, world', - 'KeyPolicy': { - 'Statement': [ - { - 'Action': [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', - ], - 'Effect': 'Allow', - 'Principal': { - 'AWS': { - 'Fn::Join': [ - '', - [ - 'arn:', - { - 'Ref': 'AWS::Partition', - }, - ':iam::', - { - 'Ref': 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - 'Resource': '*', - }, - ], - 'Version': '2012-10-17', - }, - }, - 'DeletionPolicy': 'Retain', - 'UpdateReplacePolicy': 'Retain', - }, - 'MyBucketF68F3FF0': { - 'Type': 'AWS::S3::Bucket', - 'Properties': { - 'BucketEncryption': { - 'ServerSideEncryptionConfiguration': [ - { - 'ServerSideEncryptionByDefault': { - 'KMSMasterKeyID': { - 'Fn::GetAtt': [ - 'MyKey6AB29FA6', - 'Arn', - ], - }, - 'SSEAlgorithm': 'aws:kms', - }, - }, - ], + expect(stack).toHaveResource('AWS::KMS::Key'); + + expect(stack).toHaveResource('AWS::S3::Bucket', { + 'BucketEncryption': { + 'ServerSideEncryptionConfiguration': [ + { + 'ServerSideEncryptionByDefault': { + 'KMSMasterKeyID': { + 'Fn::GetAtt': [ + 'MyKey6AB29FA6', + 'Arn', + ], + }, + 'SSEAlgorithm': 'aws:kms', }, }, - 'DeletionPolicy': 'Retain', - 'UpdateReplacePolicy': 'Retain', - }, + ], }, }); - test.done(); - }, - 'bucketKeyEnabled can be enabled'(test: Test) { + }); + + test('bucketKeyEnabled can be enabled', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { bucketKeyEnabled: true, encryption: s3.BucketEncryption.KMS }); - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { 'BucketEncryption': { 'ServerSideEncryptionConfiguration': [ { @@ -357,30 +299,30 @@ nodeunitShim({ }, ], }, - }), - ); - test.done(); - }, + }); + + + }); - 'throws error if bucketKeyEnabled is set, but encryption is not KMS'(test: Test) { + test('throws error if bucketKeyEnabled is set, but encryption is not KMS', () => { const stack = new cdk.Stack(); - test.throws(() => { + expect(() => { new s3.Bucket(stack, 'MyBucket', { bucketKeyEnabled: true, encryption: s3.BucketEncryption.S3_MANAGED }); - }, "bucketKeyEnabled is specified, so 'encryption' must be set to KMS (value: S3MANAGED)"); - test.throws(() => { + }).toThrow("bucketKeyEnabled is specified, so 'encryption' must be set to KMS (value: S3MANAGED)"); + expect(() => { new s3.Bucket(stack, 'MyBucket3', { bucketKeyEnabled: true }); - }, "bucketKeyEnabled is specified, so 'encryption' must be set to KMS (value: NONE)"); - test.done(); - }, + }).toThrow("bucketKeyEnabled is specified, so 'encryption' must be set to KMS (value: NONE)"); - 'bucket with versioning turned on'(test: Test) { + }); + + test('bucket with versioning turned on', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { versioned: true, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -394,16 +336,16 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'bucket with block public access set to BlockAll'(test: Test) { + }); + + test('bucket with block public access set to BlockAll', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -420,16 +362,16 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'bucket with block public access set to BlockAcls'(test: Test) { + }); + + test('bucket with block public access set to BlockAcls', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -444,16 +386,16 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'bucket with custom block public access setting'(test: Test) { + }); + + test('bucket with custom block public access setting', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { blockPublicAccess: new s3.BlockPublicAccess({ restrictPublicBuckets: true }), }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -467,16 +409,16 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'bucket with custom canned access control'(test: Test) { + }); + + test('bucket with custom canned access control', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { accessControl: s3.BucketAccessControl.LOG_DELIVERY_WRITE, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -488,12 +430,12 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'permissions': { + }); + + describe('permissions', () => { - 'addPermission creates a bucket policy'(test: Test) { + test('addPermission creates a bucket policy', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.UNENCRYPTED }); @@ -503,7 +445,7 @@ nodeunitShim({ principals: [new iam.AnyPrincipal()], })); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -532,10 +474,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'forBucket returns a permission statement associated with the bucket\'s ARN'(test: Test) { + }); + + test('forBucket returns a permission statement associated with the bucket\'s ARN', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.UNENCRYPTED }); @@ -546,17 +488,17 @@ nodeunitShim({ principals: [new iam.AnyPrincipal()], }); - test.deepEqual(stack.resolve(x.toStatementJson()), { + expect(stack.resolve(x.toStatementJson())).toEqual({ Action: 's3:ListBucket', Effect: 'Allow', Principal: '*', Resource: { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, }); - test.done(); - }, - 'arnForObjects returns a permission statement associated with objects in the bucket'(test: Test) { + }); + + test('arnForObjects returns a permission statement associated with objects in the bucket', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.UNENCRYPTED }); @@ -567,7 +509,7 @@ nodeunitShim({ principals: [new iam.AnyPrincipal()], }); - test.deepEqual(stack.resolve(p.toStatementJson()), { + expect(stack.resolve(p.toStatementJson())).toEqual({ Action: 's3:GetObject', Effect: 'Allow', Principal: '*', @@ -579,10 +521,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'arnForObjects accepts multiple arguments and FnConcats them'(test: Test) { + }); + + test('arnForObjects accepts multiple arguments and FnConcats them', () => { const stack = new cdk.Stack(); @@ -598,7 +540,7 @@ nodeunitShim({ principals: [new iam.AnyPrincipal()], }); - test.deepEqual(stack.resolve(p.toStatementJson()), { + expect(stack.resolve(p.toStatementJson())).toEqual({ Action: 's3:GetObject', Effect: 'Allow', Principal: '*', @@ -617,18 +559,18 @@ nodeunitShim({ }, }); - test.done(); - }, - }, - 'removal policy can be used to specify behavior upon delete'(test: Test) { + }); + }); + + test('removal policy can be used to specify behavior upon delete', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { removalPolicy: cdk.RemovalPolicy.RETAIN, encryption: s3.BucketEncryption.UNENCRYPTED, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ Resources: { MyBucketF68F3FF0: { Type: 'AWS::S3::Bucket', @@ -638,12 +580,12 @@ nodeunitShim({ }, }); - test.done(); - }, - 'import/export': { + }); + + describe('import/export', () => { - 'static import(ref) allows importing an external/existing bucket'(test: Test) { + test('static import(ref) allows importing an external/existing bucket', () => { const stack = new cdk.Stack(); const bucketArn = 'arn:aws:s3:::my-bucket'; @@ -663,21 +605,21 @@ nodeunitShim({ }); // it is possible to obtain a permission statement for a ref - test.deepEqual(p.toStatementJson(), { + expect(p.toStatementJson()).toEqual({ Action: 's3:ListBucket', Effect: 'Allow', Principal: '*', Resource: 'arn:aws:s3:::my-bucket', }); - test.deepEqual(bucket.bucketArn, bucketArn); - test.deepEqual(stack.resolve(bucket.bucketName), 'my-bucket'); + expect(bucket.bucketArn).toEqual(bucketArn); + expect(stack.resolve(bucket.bucketName)).toEqual('my-bucket'); + + expect(SynthUtils.synthesize(stack).template).toEqual({}); - test.deepEqual(SynthUtils.synthesize(stack).template, {}, 'the ref is not a real resource'); - test.done(); - }, + }); - 'import does not create any resources'(test: Test) { + test('import does not create any resources', () => { const stack = new cdk.Stack(); const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { bucketArn: 'arn:aws:s3:::my-bucket' }); bucket.addToResourcePolicy(new iam.PolicyStatement({ @@ -687,11 +629,11 @@ nodeunitShim({ })); // at this point we technically didn't create any resources in the consuming stack. - expect(stack).toMatch({}); - test.done(); - }, + expect(stack).toMatchTemplate({}); + + }); - 'import can also be used to import arbitrary ARNs'(test: Test) { + test('import can also be used to import arbitrary ARNs', () => { const stack = new cdk.Stack(); const bucket = s3.Bucket.fromBucketAttributes(stack, 'ImportedBucket', { bucketArn: 'arn:aws:s3:::my-bucket' }); bucket.addToResourcePolicy(new iam.PolicyStatement({ resources: ['*'], actions: ['*'] })); @@ -704,7 +646,7 @@ nodeunitShim({ actions: ['s3:*'], })); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyUserDC45028B': { 'Type': 'AWS::IAM::User', @@ -733,10 +675,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'import can explicitly set bucket region'(test: Test) { + }); + + test('import can explicitly set bucket region', () => { const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' }, }); @@ -746,19 +688,19 @@ nodeunitShim({ region: 'eu-west-1', }); - test.equals(bucket.bucketRegionalDomainName, `myBucket.s3.eu-west-1.${stack.urlSuffix}`); - test.equals(bucket.bucketWebsiteDomainName, `myBucket.s3-website-eu-west-1.${stack.urlSuffix}`); + expect(bucket.bucketRegionalDomainName).toEqual(`myBucket.s3.eu-west-1.${stack.urlSuffix}`); + expect(bucket.bucketWebsiteDomainName).toEqual(`myBucket.s3-website-eu-west-1.${stack.urlSuffix}`); - test.done(); - }, - }, - 'grantRead'(test: Test) { + }); + }); + + test('grantRead', () => { const stack = new cdk.Stack(); const reader = new iam.User(stack, 'Reader'); const bucket = new s3.Bucket(stack, 'MyBucket'); bucket.grantRead(reader); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'ReaderF7BF189D': { 'Type': 'AWS::IAM::User', @@ -816,17 +758,17 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'grantReadWrite': { - 'can be used to grant reciprocal permissions to an identity'(test: Test) { + }); + + describe('grantReadWrite', () => { + test('can be used to grant reciprocal permissions to an identity', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); const user = new iam.User(stack, 'MyUser'); bucket.grantReadWrite(user); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -888,10 +830,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'grant permissions to non-identity principal'(test: Test) { + }); + + test('grant permissions to non-identity principal', () => { // GIVEN const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS }); @@ -900,7 +842,7 @@ nodeunitShim({ bucket.grantRead(new iam.OrganizationPrincipal('o-1234')); // THEN - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { PolicyDocument: { 'Version': '2012-10-17', 'Statement': [ @@ -916,25 +858,11 @@ nodeunitShim({ }, ], }, - })); + }); - expect(stack).to(haveResource('AWS::KMS::Key', { + expect(stack).toHaveResourceLike('AWS::KMS::Key', { 'KeyPolicy': { - 'Statement': [ - { - 'Action': ['kms:Create*', 'kms:Describe*', 'kms:Enable*', 'kms:List*', 'kms:Put*', 'kms:Update*', - 'kms:Revoke*', 'kms:Disable*', 'kms:Get*', 'kms:Delete*', 'kms:ScheduleKeyDeletion', 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', 'kms:TagResource', 'kms:UntagResource'], - 'Effect': 'Allow', - 'Principal': { - 'AWS': { - 'Fn::Join': ['', [ - 'arn:', { 'Ref': 'AWS::Partition' }, ':iam::', { 'Ref': 'AWS::AccountId' }, ':root', - ]], - }, - }, - 'Resource': '*', - }, + 'Statement': arrayWith( { 'Action': ['kms:Decrypt', 'kms:DescribeKey'], 'Effect': 'Allow', @@ -942,22 +870,22 @@ nodeunitShim({ 'Principal': '*', 'Condition': { 'StringEquals': { 'aws:PrincipalOrgID': 'o-1234' } }, }, - ], + ), 'Version': '2012-10-17', }, - })); + }); - test.done(); - }, - 'if an encryption key is included, encrypt/decrypt permissions are also added both ways'(test: Test) { - const stack = new cdk.Stack(); + }); + + testLegacyBehavior('if an encryption key is included, encrypt/decrypt permissions are also added both ways', cdk.App, (app) => { + const stack = new cdk.Stack(app); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS }); const user = new iam.User(stack, 'MyUser'); bucket.grantReadWrite(user); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketKeyC17130CF': { 'Type': 'AWS::KMS::Key', @@ -1122,11 +1050,9 @@ nodeunitShim({ }, }, }); + }); - test.done(); - }, - - 'does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled'(test: Test) { + test('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', () => { const app = new cdk.App({ context: { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true, @@ -1138,7 +1064,7 @@ nodeunitShim({ bucket.grantReadWrite(user); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1162,184 +1088,82 @@ nodeunitShim({ }, ], }, - })); + }); - test.done(); - }, - }, - 'grantWrite': { - 'with KMS key has appropriate permissions for multipart uploads'(test: Test) { + }); + }); + + describe('grantWrite', () => { + test('with KMS key has appropriate permissions for multipart uploads', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS }); const user = new iam.User(stack, 'MyUser'); bucket.grantWrite(user); - expect(stack).toMatch({ - 'Resources': { - 'MyBucketKeyC17130CF': { - 'Type': 'AWS::KMS::Key', - 'Properties': { - 'KeyPolicy': { - 'Statement': [ - { - 'Action': [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', - ], - 'Effect': 'Allow', - 'Principal': { - 'AWS': { - 'Fn::Join': [ - '', - [ - 'arn:', - { - 'Ref': 'AWS::Partition', - }, - ':iam::', - { - 'Ref': 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - 'Resource': '*', - }, - { - 'Action': [ - 'kms:Encrypt', - 'kms:ReEncrypt*', - 'kms:GenerateDataKey*', - 'kms:Decrypt', - ], - 'Effect': 'Allow', - 'Principal': { - 'AWS': { - 'Fn::GetAtt': [ - 'MyUserDC45028B', - 'Arn', - ], - }, - }, - 'Resource': '*', - }, - ], - 'Version': '2012-10-17', - }, - 'Description': 'Created by Default/MyBucket', - }, - 'UpdateReplacePolicy': 'Retain', - 'DeletionPolicy': 'Retain', - }, - 'MyBucketF68F3FF0': { - 'Type': 'AWS::S3::Bucket', - 'Properties': { - 'BucketEncryption': { - 'ServerSideEncryptionConfiguration': [ - { - 'ServerSideEncryptionByDefault': { - 'KMSMasterKeyID': { - 'Fn::GetAtt': [ - 'MyBucketKeyC17130CF', - 'Arn', - ], - }, - 'SSEAlgorithm': 'aws:kms', - }, - }, - ], - }, - }, - 'UpdateReplacePolicy': 'Retain', - 'DeletionPolicy': 'Retain', - }, - 'MyUserDC45028B': { - 'Type': 'AWS::IAM::User', - }, - 'MyUserDefaultPolicy7B897426': { - 'Type': 'AWS::IAM::Policy', - 'Properties': { - 'PolicyDocument': { - 'Statement': [ - { - 'Action': [ - 's3:DeleteObject*', - 's3:PutObject*', - 's3:Abort*', - ], - 'Effect': 'Allow', - 'Resource': [ + expect(stack).toHaveResource('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 's3:DeleteObject*', + 's3:PutObject*', + 's3:Abort*', + ], + 'Effect': 'Allow', + 'Resource': [ + { + 'Fn::GetAtt': [ + 'MyBucketF68F3FF0', + 'Arn', + ], + }, + { + 'Fn::Join': [ + '', + [ { 'Fn::GetAtt': [ 'MyBucketF68F3FF0', 'Arn', ], }, - { - 'Fn::Join': [ - '', - [ - { - 'Fn::GetAtt': [ - 'MyBucketF68F3FF0', - 'Arn', - ], - }, - '/*', - ], - ], - }, - ], - }, - { - 'Action': [ - 'kms:Encrypt', - 'kms:ReEncrypt*', - 'kms:GenerateDataKey*', - 'kms:Decrypt', + '/*', ], - 'Effect': 'Allow', - 'Resource': { - 'Fn::GetAtt': [ - 'MyBucketKeyC17130CF', - 'Arn', - ], - }, - }, - ], - 'Version': '2012-10-17', - }, - 'PolicyName': 'MyUserDefaultPolicy7B897426', - 'Users': [ - { - 'Ref': 'MyUserDC45028B', + ], }, ], }, - }, + { + 'Action': [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + 'kms:Decrypt', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': [ + 'MyBucketKeyC17130CF', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', }, + 'PolicyName': 'MyUserDefaultPolicy7B897426', + 'Users': [ + { + 'Ref': 'MyUserDC45028B', + }, + ], }); - test.done(); - }, - 'does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled'(test: Test) { + }); + + test('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', () => { const app = new cdk.App({ context: { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true, @@ -1351,7 +1175,7 @@ nodeunitShim({ bucket.grantWrite(user); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1372,14 +1196,14 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, - }, + }); + }); - 'grantPut': { - 'does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled'(test: Test) { + describe('grantPut', () => { + test('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', () => { const app = new cdk.App({ context: { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true, @@ -1391,7 +1215,7 @@ nodeunitShim({ bucket.grantPut(user); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1408,13 +1232,13 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, - }, + }); + }); - 'more grants'(test: Test) { + test('more grants', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS }); const putter = new iam.User(stack, 'Putter'); @@ -1428,13 +1252,13 @@ nodeunitShim({ const resources = SynthUtils.synthesize(stack).template.Resources; const actions = (id: string) => resources[id].Properties.PolicyDocument.Statement[0].Action; - test.deepEqual(actions('WriterDefaultPolicyDC585BCE'), ['s3:DeleteObject*', 's3:PutObject*', 's3:Abort*']); - test.deepEqual(actions('PutterDefaultPolicyAB138DD3'), ['s3:PutObject*', 's3:Abort*']); - test.deepEqual(actions('DeleterDefaultPolicyCD33B8A0'), 's3:DeleteObject*'); - test.done(); - }, + expect(actions('WriterDefaultPolicyDC585BCE')).toEqual(['s3:DeleteObject*', 's3:PutObject*', 's3:Abort*']); + expect(actions('PutterDefaultPolicyAB138DD3')).toEqual(['s3:PutObject*', 's3:Abort*']); + expect(actions('DeleterDefaultPolicyCD33B8A0')).toEqual('s3:DeleteObject*'); + + }); - 'grantDelete, with a KMS Key'(test: Test) { + test('grantDelete, with a KMS Key', () => { // given const stack = new cdk.Stack(); const key = new kms.Key(stack, 'MyKey'); @@ -1449,7 +1273,7 @@ nodeunitShim({ bucket.grantDelete(deleter); // then - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1473,13 +1297,13 @@ nodeunitShim({ ], 'Version': '2012-10-17', }, - })); + }); - test.done(); - }, - 'cross-stack permissions': { - 'in the same account and region'(test: Test) { + }); + + describe('cross-stack permissions', () => { + test('in the same account and region', () => { const app = new cdk.App(); const stackA = new cdk.Stack(app, 'stackA'); const bucketFromStackA = new s3.Bucket(stackA, 'MyBucket'); @@ -1488,7 +1312,7 @@ nodeunitShim({ const user = new iam.User(stackB, 'UserWhoNeedsAccess'); bucketFromStackA.grantRead(user); - expect(stackA).toMatch({ + expect(stackA).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -1511,7 +1335,7 @@ nodeunitShim({ }, }); - expect(stackB).toMatch({ + expect(stackB).toMatchTemplate({ 'Resources': { 'UserWhoNeedsAccessF8959C3D': { 'Type': 'AWS::IAM::User', @@ -1559,10 +1383,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'in different accounts'(test: Test) { + }); + + test('in different accounts', () => { // given const stackA = new cdk.Stack(undefined, 'StackA', { env: { account: '123456789012' } }); const bucketFromStackA = new s3.Bucket(stackA, 'MyBucket', { @@ -1579,7 +1403,7 @@ nodeunitShim({ bucketFromStackA.grantRead(roleFromStackB); // then - expect(stackA).to(haveResourceLike('AWS::S3::BucketPolicy', { + expect(stackA).toHaveResourceLike('AWS::S3::BucketPolicy', { 'PolicyDocument': { 'Statement': [ { @@ -1606,9 +1430,9 @@ nodeunitShim({ }, ], }, - })); + }); - expect(stackB).to(haveResourceLike('AWS::IAM::Policy', { + expect(stackB).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1647,12 +1471,12 @@ nodeunitShim({ }, ], }, - })); + }); - test.done(); - }, - 'in different accounts, with a KMS Key'(test: Test) { + }); + + test('in different accounts, with a KMS Key', () => { // given const stackA = new cdk.Stack(undefined, 'StackA', { env: { account: '123456789012' } }); const key = new kms.Key(stackA, 'MyKey'); @@ -1672,7 +1496,7 @@ nodeunitShim({ bucketFromStackA.grantRead(roleFromStackB); // then - expect(stackA).to(haveResourceLike('AWS::KMS::Key', { + expect(stackA).toHaveResourceLike('AWS::KMS::Key', { 'KeyPolicy': { 'Statement': [ { @@ -1701,9 +1525,9 @@ nodeunitShim({ }, ], }, - })); + }); - expect(stackB).to(haveResourceLike('AWS::IAM::Policy', { + expect(stackB).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -1719,13 +1543,13 @@ nodeunitShim({ }, ], }, - })); + }); - test.done(); - }, - }, - 'urlForObject returns a token with the S3 URL of the token'(test: Test) { + }); + }); + + test('urlForObject returns a token with the S3 URL of the token', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -1733,7 +1557,7 @@ nodeunitShim({ new cdk.CfnOutput(stack, 'MyFileURL', { value: bucket.urlForObject('my/file.txt') }); new cdk.CfnOutput(stack, 'YourFileURL', { value: bucket.urlForObject('/your/file.txt') }); // "/" is optional - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -1810,10 +1634,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 's3UrlForObject returns a token with the S3 URL of the token'(test: Test) { + }); + + test('s3UrlForObject returns a token with the S3 URL of the token', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -1821,7 +1645,7 @@ nodeunitShim({ new cdk.CfnOutput(stack, 'MyFileS3URL', { value: bucket.s3UrlForObject('my/file.txt') }); new cdk.CfnOutput(stack, 'YourFileS3URL', { value: bucket.s3UrlForObject('/your/file.txt') }); // "/" is optional - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -1874,11 +1698,11 @@ nodeunitShim({ }, }); - test.done(); - }, - 'grantPublicAccess': { - 'by default, grants s3:GetObject to all objects'(test: Test) { + }); + + describe('grantPublicAccess', () => { + test('by default, grants s3:GetObject to all objects', () => { // GIVEN const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'b'); @@ -1887,7 +1711,7 @@ nodeunitShim({ bucket.grantPublicAccess(); // THEN - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { 'PolicyDocument': { 'Statement': [ { @@ -1899,11 +1723,11 @@ nodeunitShim({ ], 'Version': '2012-10-17', }, - })); - test.done(); - }, + }); + + }); - '"keyPrefix" can be used to only grant access to certain objects'(test: Test) { + test('"keyPrefix" can be used to only grant access to certain objects', () => { // GIVEN const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'b'); @@ -1912,7 +1736,7 @@ nodeunitShim({ bucket.grantPublicAccess('only/access/these/*'); // THEN - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { 'PolicyDocument': { 'Statement': [ { @@ -1924,11 +1748,11 @@ nodeunitShim({ ], 'Version': '2012-10-17', }, - })); - test.done(); - }, + }); + + }); - '"allowedActions" can be used to specify actions explicitly'(test: Test) { + test('"allowedActions" can be used to specify actions explicitly', () => { // GIVEN const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'b'); @@ -1937,7 +1761,7 @@ nodeunitShim({ bucket.grantPublicAccess('*', 's3:GetObject', 's3:PutObject'); // THEN - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { 'PolicyDocument': { 'Statement': [ { @@ -1949,11 +1773,11 @@ nodeunitShim({ ], 'Version': '2012-10-17', }, - })); - test.done(); - }, + }); - 'returns the PolicyStatement which can be then customized'(test: Test) { + }); + + test('returns the PolicyStatement which can be then customized', () => { // GIVEN const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'b'); @@ -1963,7 +1787,7 @@ nodeunitShim({ result.resourceStatement!.addCondition('IpAddress', { 'aws:SourceIp': '54.240.143.0/24' }); // THEN - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { 'PolicyDocument': { 'Statement': [ { @@ -1978,11 +1802,11 @@ nodeunitShim({ ], 'Version': '2012-10-17', }, - })); - test.done(); - }, + }); - 'throws when blockPublicPolicy is set to true'(test: Test) { + }); + + test('throws when blockPublicPolicy is set to true', () => { // GIVEN const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket', { @@ -1990,62 +1814,62 @@ nodeunitShim({ }); // THEN - test.throws(() => bucket.grantPublicAccess(), /blockPublicPolicy/); + expect(() => bucket.grantPublicAccess()).toThrow(/blockPublicPolicy/); - test.done(); - }, - }, - 'website configuration': { - 'only index doc'(test: Test) { + }); + }); + + describe('website configuration', () => { + test('only index doc', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index2.html', }); - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { WebsiteConfiguration: { IndexDocument: 'index2.html', }, - })); - test.done(); - }, - 'fails if only error doc is specified'(test: Test) { + }); + + }); + test('fails if only error doc is specified', () => { const stack = new cdk.Stack(); - test.throws(() => { + expect(() => { new s3.Bucket(stack, 'Website', { websiteErrorDocument: 'error.html', }); - }, /"websiteIndexDocument" is required if "websiteErrorDocument" is set/); - test.done(); - }, - 'error and index docs'(test: Test) { + }).toThrow(/"websiteIndexDocument" is required if "websiteErrorDocument" is set/); + + }); + test('error and index docs', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index2.html', websiteErrorDocument: 'error.html', }); - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { WebsiteConfiguration: { IndexDocument: 'index2.html', ErrorDocument: 'error.html', }, - })); - test.done(); - }, - 'exports the WebsiteURL'(test: Test) { + }); + + }); + test('exports the WebsiteURL', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index.html', }); - test.deepEqual(stack.resolve(bucket.bucketWebsiteUrl), { 'Fn::GetAtt': ['Website32962D0B', 'WebsiteURL'] }); - test.done(); - }, - 'exports the WebsiteDomain'(test: Test) { + expect(stack.resolve(bucket.bucketWebsiteUrl)).toEqual({ 'Fn::GetAtt': ['Website32962D0B', 'WebsiteURL'] }); + + }); + test('exports the WebsiteDomain', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index.html', }); - test.deepEqual(stack.resolve(bucket.bucketWebsiteDomainName), { + expect(stack.resolve(bucket.bucketWebsiteDomainName)).toEqual({ 'Fn::Select': [ 2, { @@ -2053,12 +1877,12 @@ nodeunitShim({ }, ], }); - test.done(); - }, - 'exports the WebsiteURL for imported buckets'(test: Test) { + + }); + test('exports the WebsiteURL for imported buckets', () => { const stack = new cdk.Stack(); const bucket = s3.Bucket.fromBucketName(stack, 'Website', 'my-test-bucket'); - test.deepEqual(stack.resolve(bucket.bucketWebsiteUrl), { + expect(stack.resolve(bucket.bucketWebsiteUrl)).toEqual({ 'Fn::Join': [ '', [ @@ -2069,7 +1893,7 @@ nodeunitShim({ ], ], }); - test.deepEqual(stack.resolve(bucket.bucketWebsiteDomainName), { + expect(stack.resolve(bucket.bucketWebsiteDomainName)).toEqual({ 'Fn::Join': [ '', [ @@ -2080,19 +1904,19 @@ nodeunitShim({ ], ], }); - test.done(); - }, - 'exports the WebsiteURL for imported buckets with url'(test: Test) { + + }); + test('exports the WebsiteURL for imported buckets with url', () => { const stack = new cdk.Stack(); const bucket = s3.Bucket.fromBucketAttributes(stack, 'Website', { bucketName: 'my-test-bucket', bucketWebsiteUrl: 'http://my-test-bucket.my-test.suffix', }); - test.deepEqual(stack.resolve(bucket.bucketWebsiteUrl), 'http://my-test-bucket.my-test.suffix'); - test.deepEqual(stack.resolve(bucket.bucketWebsiteDomainName), 'my-test-bucket.my-test.suffix'); - test.done(); - }, - 'adds RedirectAllRequestsTo property'(test: Test) { + expect(stack.resolve(bucket.bucketWebsiteUrl)).toEqual('http://my-test-bucket.my-test.suffix'); + expect(stack.resolve(bucket.bucketWebsiteDomainName)).toEqual('my-test-bucket.my-test.suffix'); + + }); + test('adds RedirectAllRequestsTo property', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'Website', { websiteRedirect: { @@ -2100,19 +1924,19 @@ nodeunitShim({ protocol: s3.RedirectProtocol.HTTPS, }, }); - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { WebsiteConfiguration: { RedirectAllRequestsTo: { HostName: 'www.example.com', Protocol: 'https', }, }, - })); - test.done(); - }, - 'fails if websiteRedirect and websiteIndex and websiteError are specified'(test: Test) { + }); + + }); + test('fails if websiteRedirect and websiteIndex and websiteError are specified', () => { const stack = new cdk.Stack(); - test.throws(() => { + expect(() => { new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index.html', websiteErrorDocument: 'error.html', @@ -2120,22 +1944,22 @@ nodeunitShim({ hostName: 'www.example.com', }, }); - }, /"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used/); - test.done(); - }, - 'fails if websiteRedirect and websiteRoutingRules are specified'(test: Test) { + }).toThrow(/"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used/); + + }); + test('fails if websiteRedirect and websiteRoutingRules are specified', () => { const stack = new cdk.Stack(); - test.throws(() => { + expect(() => { new s3.Bucket(stack, 'Website', { websiteRoutingRules: [], websiteRedirect: { hostName: 'www.example.com', }, }); - }, /"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used/); - test.done(); - }, - 'adds RedirectRules property'(test: Test) { + }).toThrow(/"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used/); + + }); + test('adds RedirectRules property', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'Website', { websiteRoutingRules: [{ @@ -2149,7 +1973,7 @@ nodeunitShim({ }, }], }); - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { WebsiteConfiguration: { RoutingRules: [{ RedirectRule: { @@ -2164,40 +1988,40 @@ nodeunitShim({ }, }], }, - })); - test.done(); - }, - 'fails if routingRule condition object is empty'(test: Test) { + }); + + }); + test('fails if routingRule condition object is empty', () => { const stack = new cdk.Stack(); - test.throws(() => { + expect(() => { new s3.Bucket(stack, 'Website', { websiteRoutingRules: [{ httpRedirectCode: '303', condition: {}, }], }); - }, /The condition property cannot be an empty object/); - test.done(); - }, - 'isWebsite set properly with': { - 'only index doc'(test: Test) { + }).toThrow(/The condition property cannot be an empty object/); + + }); + describe('isWebsite set properly with', () => { + test('only index doc', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index2.html', }); - test.equal(bucket.isWebsite, true); - test.done(); - }, - 'error and index docs'(test: Test) { + expect(bucket.isWebsite).toEqual(true); + + }); + test('error and index docs', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'Website', { websiteIndexDocument: 'index2.html', websiteErrorDocument: 'error.html', }); - test.equal(bucket.isWebsite, true); - test.done(); - }, - 'redirects'(test: Test) { + expect(bucket.isWebsite).toEqual(true); + + }); + test('redirects', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'Website', { websiteRedirect: { @@ -2205,36 +2029,36 @@ nodeunitShim({ protocol: s3.RedirectProtocol.HTTPS, }, }); - test.equal(bucket.isWebsite, true); - test.done(); - }, - 'no website properties set'(test: Test) { + expect(bucket.isWebsite).toEqual(true); + + }); + test('no website properties set', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'Website'); - test.equal(bucket.isWebsite, false); - test.done(); - }, - 'imported website buckets'(test: Test) { + expect(bucket.isWebsite).toEqual(false); + + }); + test('imported website buckets', () => { const stack = new cdk.Stack(); const bucket = s3.Bucket.fromBucketAttributes(stack, 'Website', { bucketArn: 'arn:aws:s3:::my-bucket', isWebsite: true, }); - test.equal(bucket.isWebsite, true); - test.done(); - }, - 'imported buckets'(test: Test) { + expect(bucket.isWebsite).toEqual(true); + + }); + test('imported buckets', () => { const stack = new cdk.Stack(); const bucket = s3.Bucket.fromBucketAttributes(stack, 'NotWebsite', { bucketArn: 'arn:aws:s3:::my-bucket', }); - test.equal(bucket.isWebsite, false); - test.done(); - }, - }, - }, + expect(bucket.isWebsite).toEqual(false); - 'Bucket.fromBucketArn'(test: Test) { + }); + }); + }); + + test('Bucket.fromBucketArn', () => { // GIVEN const stack = new cdk.Stack(); @@ -2242,12 +2066,12 @@ nodeunitShim({ const bucket = s3.Bucket.fromBucketArn(stack, 'my-bucket', 'arn:aws:s3:::my_corporate_bucket'); // THEN - test.deepEqual(bucket.bucketName, 'my_corporate_bucket'); - test.deepEqual(bucket.bucketArn, 'arn:aws:s3:::my_corporate_bucket'); - test.done(); - }, + expect(bucket.bucketName).toEqual('my_corporate_bucket'); + expect(bucket.bucketArn).toEqual('arn:aws:s3:::my_corporate_bucket'); + + }); - 'Bucket.fromBucketName'(test: Test) { + test('Bucket.fromBucketName', () => { // GIVEN const stack = new cdk.Stack(); @@ -2255,24 +2079,24 @@ nodeunitShim({ const bucket = s3.Bucket.fromBucketName(stack, 'imported-bucket', 'my-bucket-name'); // THEN - test.deepEqual(bucket.bucketName, 'my-bucket-name'); - test.deepEqual(stack.resolve(bucket.bucketArn), { + expect(bucket.bucketName).toEqual('my-bucket-name'); + expect(stack.resolve(bucket.bucketArn)).toEqual({ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':s3:::my-bucket-name']], }); - test.done(); - }, - 'if a kms key is specified, it implies bucket is encrypted with kms (dah)'(test: Test) { + }); + + test('if a kms key is specified, it implies bucket is encrypted with kms (dah)', () => { // GIVEN const stack = new cdk.Stack(); const key = new kms.Key(stack, 'k'); // THEN new s3.Bucket(stack, 'b', { encryptionKey: key }); - test.done(); - }, - 'Bucket with Server Access Logs'(test: Test) { + }); + + test('Bucket with Server Access Logs', () => { // GIVEN const stack = new cdk.Stack(); @@ -2283,18 +2107,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LoggingConfiguration: { DestinationBucketName: { Ref: 'AccessLogs8B620ECA', }, }, - })); + }); - test.done(); - }, - 'Bucket with Server Access Logs with Prefix'(test: Test) { + }); + + test('Bucket with Server Access Logs with Prefix', () => { // GIVEN const stack = new cdk.Stack(); @@ -2306,19 +2130,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LoggingConfiguration: { DestinationBucketName: { Ref: 'AccessLogs8B620ECA', }, LogFilePrefix: 'hello', }, - })); + }); + - test.done(); - }, + }); - 'Access log prefix given without bucket'(test: Test) { + test('Access log prefix given without bucket', () => { // GIVEN const stack = new cdk.Stack(); @@ -2327,15 +2151,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LoggingConfiguration: { LogFilePrefix: 'hello', }, - })); - test.done(); - }, + }); + + }); - 'Bucket Allow Log delivery changes bucket Access Control should fail'(test: Test) { + test('Bucket Allow Log delivery changes bucket Access Control should fail', () => { // GIVEN const stack = new cdk.Stack(); @@ -2343,18 +2167,18 @@ nodeunitShim({ const accessLogBucket = new s3.Bucket(stack, 'AccessLogs', { accessControl: s3.BucketAccessControl.AUTHENTICATED_READ, }); - test.throws(() => + expect(() => new s3.Bucket(stack, 'MyBucket', { serverAccessLogsBucket: accessLogBucket, serverAccessLogsPrefix: 'hello', accessControl: s3.BucketAccessControl.AUTHENTICATED_READ, - }) - , /Cannot enable log delivery to this bucket because the bucket's ACL has been set and can't be changed/); + }), + ).toThrow(/Cannot enable log delivery to this bucket because the bucket's ACL has been set and can't be changed/); - test.done(); - }, - 'Defaults for an inventory bucket'(test: Test) { + }); + + test('Defaults for an inventory bucket', () => { // Given const stack = new cdk.Stack(); @@ -2369,7 +2193,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::S3::Bucket', { + expect(stack).toHaveResourceLike('AWS::S3::Bucket', { InventoryConfigurations: [ { Enabled: true, @@ -2382,9 +2206,9 @@ nodeunitShim({ Id: 'MyBucketInventory0', }, ], - })); + }); - expect(stack).to(haveResourceLike('AWS::S3::BucketPolicy', { + expect(stack).toHaveResourceLike('AWS::S3::BucketPolicy', { Bucket: { Ref: 'InventoryBucketA869B8CB' }, PolicyDocument: { Statement: arrayWith(objectLike({ @@ -2400,17 +2224,17 @@ nodeunitShim({ ], })), }, - })); + }); + - test.done(); - }, + }); - 'Bucket with objectOwnership set to BUCKET_OWNER_PREFERRED'(test: Test) { + test('Bucket with objectOwnership set to BUCKET_OWNER_PREFERRED', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -2428,15 +2252,15 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'Bucket with objectOwnership set to OBJECT_WRITER'(test: Test) { + }); + + test('Bucket with objectOwnership set to OBJECT_WRITER', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { objectOwnership: s3.ObjectOwnership.OBJECT_WRITER, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -2454,15 +2278,15 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'Bucket with objectOwnerships set to undefined'(test: Test) { + }); + + test('Bucket with objectOwnerships set to undefined', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { objectOwnership: undefined, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -2471,10 +2295,10 @@ nodeunitShim({ }, }, }); - test.done(); - }, - 'with autoDeleteObjects'(test: Test) { + }); + + test('with autoDeleteObjects', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { @@ -2482,12 +2306,12 @@ nodeunitShim({ autoDeleteObjects: true, }); - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { UpdateReplacePolicy: 'Delete', DeletionPolicy: 'Delete', - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { Bucket: { Ref: 'MyBucketF68F3FF0', }, @@ -2535,9 +2359,9 @@ nodeunitShim({ ], 'Version': '2012-10-17', }, - })); + }); - expect(stack).to(haveResource('Custom::S3AutoDeleteObjects', { + expect(stack).toHaveResource('Custom::S3AutoDeleteObjects', { 'Properties': { 'ServiceToken': { 'Fn::GetAtt': [ @@ -2552,12 +2376,12 @@ nodeunitShim({ 'DependsOn': [ 'MyBucketPolicyE7FBAC7B', ], - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - test.done(); - }, - 'with autoDeleteObjects on multiple buckets'(test: Test) { + }); + + test('with autoDeleteObjects on multiple buckets', () => { const stack = new cdk.Stack(); new s3.Bucket(stack, 'Bucket1', { @@ -2570,18 +2394,18 @@ nodeunitShim({ autoDeleteObjects: true, }); - expect(stack).to(countResources('AWS::Lambda::Function', 1)); + expect(stack).toCountResources('AWS::Lambda::Function', 1); + - test.done(); - }, + }); - 'autoDeleteObjects throws if RemovalPolicy is not DESTROY'(test: Test) { + test('autoDeleteObjects throws if RemovalPolicy is not DESTROY', () => { const stack = new cdk.Stack(); - test.throws(() => new s3.Bucket(stack, 'MyBucket', { + expect(() => new s3.Bucket(stack, 'MyBucket', { autoDeleteObjects: true, - }), /Cannot use \'autoDeleteObjects\' property on a bucket without setting removal policy to \'DESTROY\'/); + })).toThrow(/Cannot use \'autoDeleteObjects\' property on a bucket without setting removal policy to \'DESTROY\'/); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-sam/package.json b/packages/@aws-cdk/aws-sam/package.json index e447bfef72916..3e989b8edf13b 100644 --- a/packages/@aws-cdk/aws-sam/package.json +++ b/packages/@aws-cdk/aws-sam/package.json @@ -73,12 +73,12 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.4.4" + "ts-jest": "^26.5.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-sam/test/sam.test.ts b/packages/@aws-cdk/aws-sam/test/application.test.ts similarity index 100% rename from packages/@aws-cdk/aws-sam/test/sam.test.ts rename to packages/@aws-cdk/aws-sam/test/application.test.ts diff --git a/packages/@aws-cdk/aws-sam/test/function.test.ts b/packages/@aws-cdk/aws-sam/test/function.test.ts new file mode 100644 index 0000000000000..d8dade625be0e --- /dev/null +++ b/packages/@aws-cdk/aws-sam/test/function.test.ts @@ -0,0 +1,27 @@ +import '@aws-cdk/assert/jest'; +import * as cdk from '@aws-cdk/core'; +import * as sam from '../lib'; + +test("correctly chooses a string array from the type unions of the 'policies' property", () => { + const stack = new cdk.Stack(); + + new sam.CfnFunction(stack, 'MyFunction', { + codeUri: { + bucket: 'my-bucket', + key: 'my-key', + }, + runtime: 'nodejs-12.x', + handler: 'index.handler', + policies: ['AWSLambdaExecute'], + }); + + expect(stack).toHaveResourceLike('AWS::Serverless::Function', { + CodeUri: { + Bucket: 'my-bucket', + Key: 'my-key', + }, + Handler: 'index.handler', + Runtime: 'nodejs-12.x', + Policies: ['AWSLambdaExecute'], + }); +}); diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index 2b5361572e804..92ca9cad73ee2 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -267,10 +267,14 @@ abstract class SecretBase extends Resource implements ISecret { } /** - * Provides an identifier for this secret for use in IAM policies. Typically, this is just the secret ARN. - * However, secrets imported by name require a different format. + * Provides an identifier for this secret for use in IAM policies. + * If there is a full ARN, this is just the ARN; + * if we have a partial ARN -- due to either importing by secret name or partial ARN -- + * then we need to add a suffix to capture the full ARN's format. */ - protected get arnForPolicies() { return this.secretArn; } + protected get arnForPolicies() { + return this.secretFullArn ? this.secretFullArn : `${this.secretArn}-??????`; + } /** * Attach a target to this secret @@ -351,11 +355,6 @@ export class Secret extends SecretBase { 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({ diff --git a/packages/@aws-cdk/aws-secretsmanager/test/secret.test.ts b/packages/@aws-cdk/aws-secretsmanager/test/secret.test.ts index 22dae639500dd..73fc366a6eb18 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/secret.test.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/secret.test.ts @@ -4,6 +4,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; +import { testFutureBehavior, testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as secretsmanager from '../lib'; let app: cdk.App; @@ -429,10 +430,7 @@ test('secretValue', () => { describe('secretName', () => { describe('without @aws-cdk/aws-secretsmanager:parseOwnedSecretName set', () => { - test.each([undefined, 'mySecret'])('when secretName is %s', (secretName) => { - const secret = new secretsmanager.Secret(stack, 'Secret', { - secretName, - }); + function assertSecretParsing(secret: secretsmanager.Secret) { new cdk.CfnOutput(stack, 'MySecretName', { value: secret.secretName, }); @@ -442,20 +440,30 @@ describe('secretName', () => { outputName: 'MySecretName', outputValue: { 'Fn::Select': [6, { 'Fn::Split': [':', { Ref: 'SecretA720EF05' }] }] }, }); + } + + testLegacyBehavior('when secretName is undefined', cdk.App, (cdkApp) => { + stack = new cdk.Stack(cdkApp); + const secret = new secretsmanager.Secret(stack, 'Secret', { + secretName: undefined, + }); + assertSecretParsing(secret); }); - }); - describe('with @aws-cdk/aws-secretsmanager:parseOwnedSecretName set', () => { - beforeEach(() => { - app = new cdk.App({ - context: { - '@aws-cdk/aws-secretsmanager:parseOwnedSecretName': 'true', - }, + testLegacyBehavior('when secretName is defined', cdk.App, (cdkApp) => { + stack = new cdk.Stack(cdkApp); + const secret = new secretsmanager.Secret(stack, 'Secret', { + secretName: 'mySecret', }); - stack = new cdk.Stack(app); + assertSecretParsing(secret); }); + }); + + describe('with @aws-cdk/aws-secretsmanager:parseOwnedSecretName set', () => { + const flags = { '@aws-cdk/aws-secretsmanager:parseOwnedSecretName': 'true' }; + testFutureBehavior('selects the first two parts of the resource name when the name is auto-generated', flags, cdk.App, (cdkApp) => { + stack = new cdk.Stack(cdkApp); - test('selects the first two parts of the resource name when the name is auto-generated', () => { const secret = new secretsmanager.Secret(stack, 'Secret'); new cdk.CfnOutput(stack, 'MySecretName', { value: secret.secretName, @@ -474,7 +482,9 @@ describe('secretName', () => { }); }); - test('is simply the first segment when the provided secret name has no hyphens', () => { + testFutureBehavior('is simply the first segment when the provided secret name has no hyphens', flags, cdk.App, (cdkApp) => { + stack = new cdk.Stack(cdkApp); + const secret = new secretsmanager.Secret(stack, 'Secret', { secretName: 'mySecret' }); new cdk.CfnOutput(stack, 'MySecretName', { value: secret.secretName, @@ -490,12 +500,7 @@ describe('secretName', () => { }); }); - test.each([ - [2, 'my-secret'], - [3, 'my-secret-hyphenated'], - [4, 'my-secret-with-hyphens'], - ])('selects the %n parts of the resource name when the secret name is provided', (segments, secretName) => { - const secret = new secretsmanager.Secret(stack, 'Secret', { secretName }); + function assertSegments(secret: secretsmanager.Secret, segments: number) { new cdk.CfnOutput(stack, 'MySecretName', { value: secret.secretName, }); @@ -512,9 +517,29 @@ describe('secretName', () => { 'Fn::Join': ['-', secretNameSegments], }, }); + } + + testFutureBehavior('selects the 2 parts of the resource name when the secret name is provided', flags, cdk.App, (cdkApp) => { + stack = new cdk.Stack(cdkApp); + const secret = new secretsmanager.Secret(stack, 'Secret', { secretName: 'my-secret' }); + assertSegments(secret, 2); + }); + + testFutureBehavior('selects the 3 parts of the resource name when the secret name is provided', flags, cdk.App, (cdkApp) => { + stack = new cdk.Stack(cdkApp); + const secret = new secretsmanager.Secret(stack, 'Secret', { secretName: 'my-secret-hyphenated' }); + assertSegments(secret, 3); }); - test('uses existing Tokens as secret names as-is', () => { + testFutureBehavior('selects the 4 parts of the resource name when the secret name is provided', flags, cdk.App, (cdkApp) => { + stack = new cdk.Stack(cdkApp); + const secret = new secretsmanager.Secret(stack, 'Secret', { secretName: 'my-secret-with-hyphens' }); + assertSegments(secret, 4); + }); + + testFutureBehavior('uses existing Tokens as secret names as-is', flags, cdk.App, (cdkApp) => { + stack = new cdk.Stack(cdkApp); + const secret1 = new secretsmanager.Secret(stack, 'Secret1'); const secret2 = new secretsmanager.Secret(stack, 'Secret2', { secretName: secret1.secretName, @@ -628,6 +653,40 @@ test('fromSecretCompleteArn', () => { expect(stack.resolve(secret.secretValueFromJson('password'))).toEqual(`{{resolve:secretsmanager:${secretArn}:SecretString:password::}}`); }); +test('fromSecretCompleteArn - grants', () => { + // GIVEN + const secretArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret-f3gDy9'; + const secret = secretsmanager.Secret.fromSecretCompleteArn(stack, 'Secret', secretArn); + const role = new iam.Role(stack, 'Role', { assumedBy: new iam.AccountRootPrincipal() }); + + // WHEN + secret.grantRead(role); + secret.grantWrite(role); + + // THEN + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: secretArn, + }, + { + Action: [ + 'secretsmanager:PutSecretValue', + 'secretsmanager:UpdateSecret', + ], + Effect: 'Allow', + Resource: secretArn, + }], + }, + }); +}); + test('fromSecretPartialArn', () => { // GIVEN const secretArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret'; @@ -644,6 +703,40 @@ test('fromSecretPartialArn', () => { expect(stack.resolve(secret.secretValueFromJson('password'))).toEqual(`{{resolve:secretsmanager:${secretArn}:SecretString:password::}}`); }); +test('fromSecretPartialArn - grants', () => { + // GIVEN + const secretArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret'; + const secret = secretsmanager.Secret.fromSecretPartialArn(stack, 'Secret', secretArn); + const role = new iam.Role(stack, 'Role', { assumedBy: new iam.AccountRootPrincipal() }); + + // WHEN + secret.grantRead(role); + secret.grantWrite(role); + + // THEN + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: `${secretArn}-??????`, + }, + { + Action: [ + 'secretsmanager:PutSecretValue', + 'secretsmanager:UpdateSecret', + ], + Effect: 'Allow', + Resource: `${secretArn}-??????`, + }], + }, + }); +}); + describe('fromSecretAttributes', () => { test('import by attributes', () => { // GIVEN diff --git a/packages/@aws-cdk/aws-ses-actions/lib/bounce.ts b/packages/@aws-cdk/aws-ses-actions/lib/bounce.ts index b84c414c5c43e..a1a385651614a 100644 --- a/packages/@aws-cdk/aws-ses-actions/lib/bounce.ts +++ b/packages/@aws-cdk/aws-ses-actions/lib/bounce.ts @@ -100,7 +100,7 @@ export class Bounce implements ses.IReceiptRuleAction { sender: this.props.sender, smtpReplyCode: this.props.template.props.smtpReplyCode, message: this.props.template.props.message, - topicArn: this.props.topic ? this.props.topic.topicArn : undefined, + topicArn: this.props.topic?.topicArn, statusCode: this.props.template.props.statusCode, }, }; diff --git a/packages/@aws-cdk/aws-ses-actions/lib/lambda.ts b/packages/@aws-cdk/aws-ses-actions/lib/lambda.ts index 290211bf1806f..d6be68f92ce24 100644 --- a/packages/@aws-cdk/aws-ses-actions/lib/lambda.ts +++ b/packages/@aws-cdk/aws-ses-actions/lib/lambda.ts @@ -78,7 +78,7 @@ export class Lambda implements ses.IReceiptRuleAction { lambdaAction: { functionArn: this.props.function.functionArn, invocationType: this.props.invocationType, - topicArn: this.props.topic ? this.props.topic.topicArn : undefined, + topicArn: this.props.topic?.topicArn, }, }; } diff --git a/packages/@aws-cdk/aws-ses-actions/lib/s3.ts b/packages/@aws-cdk/aws-ses-actions/lib/s3.ts index 9be2fd8750378..1f288afe2a887 100644 --- a/packages/@aws-cdk/aws-ses-actions/lib/s3.ts +++ b/packages/@aws-cdk/aws-ses-actions/lib/s3.ts @@ -92,9 +92,9 @@ export class S3 implements ses.IReceiptRuleAction { return { s3Action: { bucketName: this.props.bucket.bucketName, - kmsKeyArn: this.props.kmsKey ? this.props.kmsKey.keyArn : undefined, + kmsKeyArn: this.props.kmsKey?.keyArn, objectKeyPrefix: this.props.objectKeyPrefix, - topicArn: this.props.topic ? this.props.topic.topicArn : undefined, + topicArn: this.props.topic?.topicArn, }, }; } diff --git a/packages/@aws-cdk/aws-ses-actions/lib/stop.ts b/packages/@aws-cdk/aws-ses-actions/lib/stop.ts index ab2e9cbb91d1e..f42d206d3d350 100644 --- a/packages/@aws-cdk/aws-ses-actions/lib/stop.ts +++ b/packages/@aws-cdk/aws-ses-actions/lib/stop.ts @@ -23,7 +23,7 @@ export class Stop implements ses.IReceiptRuleAction { return { stopAction: { scope: 'RuleSet', - topicArn: this.props.topic ? this.props.topic.topicArn : undefined, + topicArn: this.props.topic?.topicArn, }, }; } diff --git a/packages/@aws-cdk/aws-ses-actions/test/actions.test.ts b/packages/@aws-cdk/aws-ses-actions/test/actions.test.ts index 3a963fcf3f24b..d3268e949b7dc 100644 --- a/packages/@aws-cdk/aws-ses-actions/test/actions.test.ts +++ b/packages/@aws-cdk/aws-ses-actions/test/actions.test.ts @@ -1,4 +1,4 @@ -import { ResourcePart } from '@aws-cdk/assert'; +import { arrayWith, ResourcePart } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -223,72 +223,30 @@ test('add s3 action', () => { }, }); - expect(stack).toHaveResource('AWS::KMS::Key', { + expect(stack).toHaveResourceLike('AWS::KMS::Key', { KeyPolicy: { - Statement: [ - { - Action: [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', - ], - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, + Statement: arrayWith({ + Action: [ + 'kms:Encrypt', + 'kms:GenerateDataKey', + ], + Condition: { + Null: { + 'kms:EncryptionContext:aws:ses:rule-name': 'false', + 'kms:EncryptionContext:aws:ses:message-id': 'false', }, - Resource: '*', - }, - { - Action: [ - 'kms:Encrypt', - 'kms:GenerateDataKey', - ], - Condition: { - Null: { - 'kms:EncryptionContext:aws:ses:rule-name': 'false', - 'kms:EncryptionContext:aws:ses:message-id': 'false', - }, - StringEquals: { - 'kms:EncryptionContext:aws:ses:source-account': { - Ref: 'AWS::AccountId', - }, + StringEquals: { + 'kms:EncryptionContext:aws:ses:source-account': { + Ref: 'AWS::AccountId', }, }, - Effect: 'Allow', - Principal: { - Service: 'ses.amazonaws.com', - }, - Resource: '*', }, - ], - Version: '2012-10-17', + Effect: 'Allow', + Principal: { + Service: 'ses.amazonaws.com', + }, + Resource: '*', + }), }, }); }); diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts index c94d4d4edd138..dcb3d011d4251 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts @@ -62,7 +62,7 @@ abstract class ReceiptRuleSetBase extends Resource implements IReceiptRuleSet { */ public addRule(id: string, options?: ReceiptRuleOptions): ReceiptRule { this.lastAddedRule = new ReceiptRule(this, id, { - after: this.lastAddedRule ? this.lastAddedRule : undefined, + after: this.lastAddedRule ?? undefined, ruleSet: this, ...options, }); diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts index 72c056d913693..5b6a276929c8e 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts @@ -124,10 +124,10 @@ export class ReceiptRule extends Resource implements IReceiptRule { }); const resource = new CfnReceiptRule(this, 'Resource', { - after: props.after ? props.after.receiptRuleName : undefined, + after: props.after?.receiptRuleName, rule: { actions: Lazy.any({ produce: () => this.renderActions() }), - enabled: props.enabled === undefined ? true : props.enabled, + enabled: props.enabled ?? true, name: this.physicalName, recipients: props.recipients, scanEnabled: props.scanEnabled, diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts index 3ecab463d2c2c..aa7581653d5ba 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts @@ -1,9 +1,13 @@ 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, Names, Stack } from '@aws-cdk/core'; +import { Names, Stack } from '@aws-cdk/core'; import { SubscriptionProps } from './subscription'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Properties for a Lambda subscription */ diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts index bac6a13859d9c..4a41c5e43feb2 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts @@ -1,9 +1,13 @@ 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, Names, Stack } from '@aws-cdk/core'; +import { Names, Stack } from '@aws-cdk/core'; import { SubscriptionProps } from './subscription'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Properties for an SQS subscription */ diff --git a/packages/@aws-cdk/aws-sns/lib/subscriber.ts b/packages/@aws-cdk/aws-sns/lib/subscriber.ts index edfd3632d415a..d63667dde46e5 100644 --- a/packages/@aws-cdk/aws-sns/lib/subscriber.ts +++ b/packages/@aws-cdk/aws-sns/lib/subscriber.ts @@ -1,7 +1,11 @@ -import { Construct } from '@aws-cdk/core'; + import { SubscriptionOptions } from './subscription'; import { ITopic } from './topic-base'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Subscription configuration */ diff --git a/packages/@aws-cdk/aws-sns/lib/topic-base.ts b/packages/@aws-cdk/aws-sns/lib/topic-base.ts index bddfde6288748..db262ef87e068 100644 --- a/packages/@aws-cdk/aws-sns/lib/topic-base.ts +++ b/packages/@aws-cdk/aws-sns/lib/topic-base.ts @@ -1,9 +1,13 @@ import * as iam from '@aws-cdk/aws-iam'; -import { Construct, IResource, Resource, Token } from '@aws-cdk/core'; +import { IResource, Resource, Token } from '@aws-cdk/core'; import { TopicPolicy } from './policy'; import { ITopicSubscription } from './subscriber'; import { Subscription } from './subscription'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Represents an SNS topic */ diff --git a/packages/@aws-cdk/aws-sqs/package.json b/packages/@aws-cdk/aws-sqs/package.json index 87c9aa9ffebff..587ebea3f0158 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -73,7 +73,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.830.0", + "aws-sdk": "^2.842.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/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 689ed0a53195f..80e1dab5ef73f 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -55,6 +55,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [Modify Instance Fleet](#modify-instance-fleet) - [Modify Instance Group](#modify-instance-group) - [Glue](#glue) +- [Glue DataBrew](#glue-databrew) - [Lambda](#lambda) - [SageMaker](#sagemaker) - [Create Training Job](#create-training-job) @@ -153,7 +154,7 @@ merge a subset of the task output to the input. Most tasks take parameters. Parameter values can either be static, supplied directly in the workflow definition (by specifying their values), or a value available at runtime in the state machine's execution (either as its input or an output of a prior state). -Parameter values available at runtime can be specified via the `Data` class, +Parameter values available at runtime can be specified via the `JsonPath` class, using methods such as `JsonPath.stringAt()`. The following example provides the field named `input` as the input to the Lambda function @@ -435,6 +436,8 @@ CPU and memory. Similarly, when you scale down the task count, Amazon ECS must d which tasks to terminate. You can apply task placement strategies and constraints to customize how Amazon ECS places and terminates tasks. Learn more about [task placement](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-placement.html) +The latest ACTIVE revision of the passed task definition is used for running the task. + The following example runs a job from a task definition on EC2 ```ts @@ -488,7 +491,9 @@ isolation by design. Learn more about [Fargate](https://aws.amazon.com/fargate/) The Fargate launch type allows you to run your containerized applications without the need to provision and manage the backend infrastructure. Just register your task definition and -Fargate launches the container for you. +Fargate launches the container for you. The latest ACTIVE revision of the passed +task definition is used for running the task. Learn more about +[Fargate Versioning](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeTaskDefinition.html) The following example runs a job from a task definition on Fargate @@ -676,6 +681,18 @@ new GlueStartJobRun(stack, 'Task', { }); ``` +## Glue DataBrew + +Step Functions supports [AWS Glue DataBrew](https://docs.aws.amazon.com/step-functions/latest/dg/connect-databrew.html) through the service integration pattern. + +You can call the [`StartJobRun`](https://docs.aws.amazon.com/databrew/latest/dg/API_StartJobRun.html) API from a `Task` state. + +```ts +new GlueDataBrewStartJobRun(stack, 'Task', { + Name: 'databrew-job', +}); +``` + ## Lambda [Invoke](https://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html) a Lambda function. diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/databrew/start-job-run.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/databrew/start-job-run.ts new file mode 100644 index 0000000000000..75b6abcb6edda --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/databrew/start-job-run.ts @@ -0,0 +1,80 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Properties for starting a job run with StartJobRun + * @experimental + */ +export interface GlueDataBrewStartJobRunProps extends sfn.TaskStateBaseProps { + + /** + * Glue DataBrew Job to run + */ + readonly name: string; +} + +/** + * Start a Job run as a Task + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-databrew.html + * @experimental + */ +export class GlueDataBrewStartJobRun extends sfn.TaskStateBase { + + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + ]; + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private readonly integrationPattern: sfn.IntegrationPattern; + + /** + * @experimental + */ + constructor(scope: Construct, id: string, private readonly props: GlueDataBrewStartJobRunProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; + + validatePatternSupported(this.integrationPattern, GlueDataBrewStartJobRun.SUPPORTED_INTEGRATION_PATTERNS); + + const actions = ['databrew:startJobRun']; + + if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { + actions.push('databrew:stopJobRun', 'databrew:listJobRuns'); + } + + this.taskPolicies = [ + new iam.PolicyStatement({ + resources: [ + cdk.Stack.of(this).formatArn({ + service: 'databrew', + resource: 'job', + // If the name comes from input, we cannot target the policy to a particular ARN prefix reliably. + resourceName: sfn.JsonPath.isEncodedJsonPath(this.props.name) ? '*' : this.props.name, + }), + ], + actions: actions, + }), + ]; + } + + /** + * Provides the Glue DataBrew Start Job Run task configuration + * @internal + */ + protected _renderTask(): any { + return { + Resource: integrationResourceArn('databrew', 'startJobRun', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + Name: this.props.name, + }), + }; + } +} + diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts index f80252cbb05f8..60f0ede44be83 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-task.ts @@ -279,7 +279,7 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { Resource: integrationResourceArn('ecs', 'runTask', this.integrationPattern), Parameters: sfn.FieldUtils.renderObject({ Cluster: this.props.cluster.clusterArn, - TaskDefinition: this.props.taskDefinition.taskDefinitionArn, + TaskDefinition: this.props.taskDefinition.family, NetworkConfiguration: this.networkConfiguration, Overrides: renderOverrides(this.props.containerOverrides), ...this.props.launchTarget.bind(this, { taskDefinition: this.props.taskDefinition, cluster: this.props.cluster }).parameters, @@ -318,7 +318,7 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { const policyStatements = [ new iam.PolicyStatement({ actions: ['ecs:RunTask'], - resources: [this.props.taskDefinition.taskDefinitionArn], + resources: [this.getTaskDefinitionFamilyArn()], }), new iam.PolicyStatement({ actions: ['ecs:StopTask', 'ecs:DescribeTasks'], @@ -348,6 +348,23 @@ export class EcsRunTask extends sfn.TaskStateBase implements ec2.IConnectable { return policyStatements; } + /** + * Returns the ARN of the task definition family by removing the + * revision from the task definition ARN + * Before - arn:aws:ecs:us-west-2:123456789012:task-definition/hello_world:8 + * After - arn:aws:ecs:us-west-2:123456789012:task-definition/hello_world + */ + private getTaskDefinitionFamilyArn(): string { + const arnComponents = cdk.Stack.of(this).parseArn(this.props.taskDefinition.taskDefinitionArn); + let { resourceName } = arnComponents; + + if (resourceName) { + resourceName = resourceName.split(':')[0]; + } + + return cdk.Stack.of(this).formatArn({ ...arnComponents, resourceName }); + } + private taskExecutionRoles(): iam.IRole[] { // Need to be able to pass both Task and Execution role, apparently const ret = new Array(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts index 4b3ecd5ca0013..2e38d60aac150 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts @@ -173,7 +173,7 @@ export class EmrCreateCluster extends sfn.TaskStateBase { constructor(scope: Construct, id: string, private readonly props: EmrCreateClusterProps) { super(scope, id, props); - this.visibleToAllUsers = this.props.visibleToAllUsers !== undefined ? this.props.visibleToAllUsers : true; + this.visibleToAllUsers = this.props.visibleToAllUsers ?? true; this.integrationPattern = props.integrationPattern || sfn.IntegrationPattern.RUN_JOB; validatePatternSupported(this.integrationPattern, EmrCreateCluster.SUPPORTED_INTEGRATION_PATTERNS); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 8e3567f2a8f88..84b790beff216 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -43,3 +43,4 @@ export * from './athena/start-query-execution'; export * from './athena/stop-query-execution'; export * from './athena/get-query-execution'; export * from './athena/get-query-results'; +export * from './databrew/start-job-run'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index ef466cb5c7498..3031ab05dc9a3 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -72,10 +72,10 @@ "pkglint": "0.0.0" }, "dependencies": { - "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", + "@aws-cdk/aws-databrew": "0.0.0", "@aws-cdk/aws-dynamodb": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", @@ -94,10 +94,10 @@ }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { - "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", + "@aws-cdk/aws-databrew": "0.0.0", "@aws-cdk/aws-dynamodb": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/databrew/integ.start-job-run.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/databrew/integ.start-job-run.expected.json new file mode 100644 index 0000000000000..4b0dedef27b9b --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/databrew/integ.start-job-run.expected.json @@ -0,0 +1,255 @@ +{ + "Resources": { + "JobOutputBucketACE3BC7B": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DataBrewRole7E60F80D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "databrew.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSGlueDataBrewServiceRole" + ], + "Path": "/", + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + "s3:ListBucket" + ], + "Effect": "Allow", + "Resource": [ + "arn:aws:s3:::databrew-public-datasets-test-region/*", + "arn:aws:s3:::databrew-public-datasets-test-region", + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "JobOutputBucketACE3BC7B", + "Arn" + ] + }, + "/*" + ] + ] + }, + { + "Fn::GetAtt": [ + "JobOutputBucketACE3BC7B", + "Arn" + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "DataBrewPolicy" + } + ] + } + }, + "DataBrewRecipe": { + "Type": "AWS::DataBrew::Recipe", + "Properties": { + "Name": "recipe-1", + "Steps": [ + { + "Action": { + "Operation": "UPPER_CASE", + "Parameters": { + "sourceColumn": "description" + } + } + }, + { + "Action": { + "Operation": "DELETE", + "Parameters": { + "sourceColumn": "doc_id" + } + } + } + ] + } + }, + "DataBrewDataset": { + "Type": "AWS::DataBrew::Dataset", + "Properties": { + "Input": { + "S3InputDefinition": { + "Bucket": "databrew-public-datasets-test-region", + "Key": "votes.csv" + } + }, + "Name": "dataset-1" + } + }, + "DataBrewProject": { + "Type": "AWS::DataBrew::Project", + "Properties": { + "DatasetName": "dataset-1", + "Name": "project-1", + "RecipeName": "recipe-1", + "RoleArn": { + "Fn::GetAtt": [ + "DataBrewRole7E60F80D", + "Arn" + ] + } + }, + "DependsOn": [ + "DataBrewDataset", + "DataBrewRecipe" + ] + }, + "DataBrewJob": { + "Type": "AWS::DataBrew::Job", + "Properties": { + "Name": "job-1", + "RoleArn": { + "Fn::GetAtt": [ + "DataBrewRole7E60F80D", + "Arn" + ] + }, + "Type": "RECIPE", + "Outputs": [ + { + "Location": { + "Bucket": { + "Ref": "JobOutputBucketACE3BC7B" + } + } + } + ], + "ProjectName": "project-1" + }, + "DependsOn": [ + "DataBrewProject" + ] + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "databrew:startJobRun", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":databrew:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":job/job-1" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Start DataBrew Job run\",\"States\":{\"Start DataBrew Job run\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::databrew:startJobRun\",\"Parameters\":{\"Name\":\"job-1\"}}},\"TimeoutSeconds\":30}" + ] + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/databrew/integ.start-job-run.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/databrew/integ.start-job-run.ts new file mode 100644 index 0000000000000..689f74a678a89 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/databrew/integ.start-job-run.ts @@ -0,0 +1,124 @@ +import * as databrew from '@aws-cdk/aws-databrew'; +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { GlueDataBrewStartJobRun } from '../../lib'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --state-machine-arn : should return execution arn + * * aws stepfunctions describe-execution --execution-arn : should return status as SUCCEEDED + */ + +class GlueDataBrewJobStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props: cdk.StackProps = {}) { + super(scope, id, props); + + const region = process.env.CDK_DEPLOY_REGION || process.env.CDK_DEFAULT_REGION; + + const outputBucket = new s3.Bucket(this, 'JobOutputBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const role = new iam.Role(this, 'DataBrew Role', { + managedPolicies: [{ + managedPolicyArn: 'arn:aws:iam::aws:policy/service-role/AWSGlueDataBrewServiceRole', + }], + path: '/', + assumedBy: new iam.ServicePrincipal('databrew.amazonaws.com'), + inlinePolicies: { + DataBrewPolicy: iam.PolicyDocument.fromJson({ + Statement: [{ + Effect: 'Allow', + Action: [ + 's3:GetObject', + 's3:PutObject', + 's3:DeleteObject', + 's3:ListBucket', + ], + Resource: [ + `arn:aws:s3:::databrew-public-datasets-${region}/*`, + `arn:aws:s3:::databrew-public-datasets-${region}`, + `${outputBucket.bucketArn}/*`, + `${outputBucket.bucketArn}`, + ], + }], + }), + }, + }); + + const recipe = new databrew.CfnRecipe(this, 'DataBrew Recipe', { + name: 'recipe-1', + steps: [ + { + action: { + operation: 'UPPER_CASE', + parameters: { + sourceColumn: 'description', + }, + }, + }, + { + action: { + operation: 'DELETE', + parameters: { + sourceColumn: 'doc_id', + }, + }, + }, + ], + }); + + const dataset = new databrew.CfnDataset(this, 'DataBrew Dataset', { + input: { + S3InputDefinition: { + Bucket: `databrew-public-datasets-${region}`, + Key: 'votes.csv', + }, + }, + name: 'dataset-1', + }); + + const project = new databrew.CfnProject(this, 'DataBrew Project', { + name: 'project-1', + roleArn: role.roleArn, + datasetName: dataset.name, + recipeName: recipe.name, + }); + project.addDependsOn(dataset); + project.addDependsOn(recipe); + + const job = new databrew.CfnJob(this, 'DataBrew Job', { + name: 'job-1', + type: 'RECIPE', + projectName: project.name, + roleArn: role.roleArn, + outputs: [{ + location: { + bucket: outputBucket.bucketName, + }, + }], + }); + job.addDependsOn(project); + + const startGlueDataBrewJob = new GlueDataBrewStartJobRun(this, 'Start DataBrew Job run', { + name: job.name, + }); + + const chain = sfn.Chain.start(startGlueDataBrewJob); + + const sm = new sfn.StateMachine(this, 'StateMachine', { + definition: chain, + timeout: cdk.Duration.seconds(30), + }); + + new cdk.CfnOutput(this, 'stateMachineArn', { + value: sm.stateMachineArn, + }); + } +} + +const app = new cdk.App(); +new GlueDataBrewJobStack(app, 'aws-stepfunctions-tasks-databrew-start-job-run-integ'); +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/databrew/start-job-run.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/databrew/start-job-run.test.ts new file mode 100644 index 0000000000000..706ea92041d7a --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/databrew/start-job-run.test.ts @@ -0,0 +1,114 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { GlueDataBrewStartJobRun } from '../../lib/databrew/start-job-run'; + +describe('Start Job Run', () => { + + test('default settings', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const task = new GlueDataBrewStartJobRun(stack, 'JobRun', { + name: 'jobName', + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::databrew:startJobRun', + ], + ], + }, + End: true, + Parameters: { + Name: 'jobName', + }, + }); + }); + + test('create job with input from task', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const task = new GlueDataBrewStartJobRun(stack, 'JobRun', { + name: sfn.JsonPath.stringAt('$.Name'), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::databrew:startJobRun', + ], + ], + }, + End: true, + Parameters: { + 'Name.$': '$.Name', + }, + }); + }); + + test('sync integrationPattern', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const task = new GlueDataBrewStartJobRun(stack, 'JobRun', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + name: 'jobName', + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::databrew:startJobRun.sync', + ], + ], + }, + End: true, + Parameters: { + Name: 'jobName', + }, + }); + }); + + + test('wait_for_task_token integrationPattern throws an error', () => { + // GIVEN + const stack = new cdk.Stack(); + + expect(() => { + new GlueDataBrewStartJobRun(stack, 'JobRun', { + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + name: 'jobName', + }); + }).toThrow(/Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE,RUN_JOB. Received: WAIT_FOR_TASK_TOKEN/i); + }); +}); + diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json index f9694026e0630..97a2e05741145 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-run-task.expected.json @@ -80,8 +80,6 @@ "ecs:Poll", "ecs:StartTelemetrySession" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -91,7 +89,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -268,8 +268,6 @@ "ecs:DescribeContainerInstances", "ecs:DescribeTasks" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -279,7 +277,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -330,14 +330,12 @@ "Code": { "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n while has_tasks(cluster, instance_arn):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\n\ndef has_tasks(cluster, instance_arn):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n tasks = instance['runningTasksCount'] + instance['pendingTasksCount']\n print('Instance %s has %s tasks' % (instance_arn, tasks))\n\n return tasks > 0\n\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" }, - "Handler": "index.lambda_handler", "Role": { "Fn::GetAtt": [ "Ec2ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole23116FA3", "Arn" ] }, - "Runtime": "python3.6", "Environment": { "Variables": { "CLUSTER": { @@ -345,6 +343,8 @@ } } }, + "Handler": "index.lambda_handler", + "Runtime": "python3.6", "Tags": [ { "Key": "Name", @@ -640,7 +640,115 @@ "Action": "ecs:RunTask", "Effect": "Allow", "Resource": { - "Ref": "TaskDef54694570" + "Fn::Join": [ + "", + [ + "arn:", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + ":", + { + "Ref": "TaskDef54694570" + } + ] + } + ] + }, + ":", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + ":", + { + "Ref": "TaskDef54694570" + } + ] + } + ] + }, + ":", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Ref": "TaskDef54694570" + } + ] + } + ] + }, + ":", + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Ref": "TaskDef54694570" + } + ] + } + ] + }, + ":", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "TaskDef54694570" + } + ] + } + ] + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "TaskDef54694570" + } + ] + } + ] + } + ] + } + ] + } + ] + ] } }, { @@ -724,11 +832,7 @@ "Arn" ] }, - "\",\"TaskDefinition\":\"", - { - "Ref": "TaskDef54694570" - }, - "\",\"Overrides\":{\"ContainerOverrides\":[{\"Name\":\"TheContainer\",\"Environment\":[{\"Name\":\"SOME_KEY\",\"Value.$\":\"$.SomeKey\"}]}]},\"LaunchType\":\"EC2\"}}}}" + "\",\"TaskDefinition\":\"awssfntasksecsec2integTaskDefFAFE2BE7\",\"Overrides\":{\"ContainerOverrides\":[{\"Name\":\"TheContainer\",\"Environment\":[{\"Name\":\"SOME_KEY\",\"Value.$\":\"$.SomeKey\"}]}]},\"LaunchType\":\"EC2\"}}}}" ] ] } @@ -752,4 +856,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json index 080638d46ddcb..a2d79417c61df 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-run-task.expected.json @@ -189,7 +189,115 @@ "Action": "ecs:RunTask", "Effect": "Allow", "Resource": { - "Ref": "TaskDef54694570" + "Fn::Join": [ + "", + [ + "arn:", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + ":", + { + "Ref": "TaskDef54694570" + } + ] + } + ] + }, + ":", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + ":", + { + "Ref": "TaskDef54694570" + } + ] + } + ] + }, + ":", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Ref": "TaskDef54694570" + } + ] + } + ] + }, + ":", + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Ref": "TaskDef54694570" + } + ] + } + ] + }, + ":", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "TaskDef54694570" + } + ] + } + ] + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Fn::Select": [ + 5, + { + "Fn::Split": [ + ":", + { + "Ref": "TaskDef54694570" + } + ] + } + ] + } + ] + } + ] + } + ] + ] } }, { @@ -273,11 +381,7 @@ "Arn" ] }, - "\",\"TaskDefinition\":\"", - { - "Ref": "TaskDef54694570" - }, - "\",\"NetworkConfiguration\":{\"AwsvpcConfiguration\":{\"AssignPublicIp\":\"ENABLED\",\"Subnets\":[\"subnet-e19455ca\",\"subnet-e0c24797\",\"subnet-ccd77395\"],\"SecurityGroups\":[\"", + "\",\"TaskDefinition\":\"awssfntasksecsfargateintegTaskDefD0F4AD10\",\"NetworkConfiguration\":{\"AwsvpcConfiguration\":{\"AssignPublicIp\":\"ENABLED\",\"Subnets\":[\"subnet-e19455ca\",\"subnet-e0c24797\",\"subnet-ccd77395\"],\"SecurityGroups\":[\"", { "Fn::GetAtt": [ "FargateTaskSecurityGroup0BBB27CB", @@ -302,4 +406,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts index a60a993967bee..ff2072b6b2ebf 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/run-tasks.test.ts @@ -92,7 +92,7 @@ test('Running a Fargate Task', () => { }, }, PlatformVersion: '1.4.0', - TaskDefinition: { Ref: 'TD49C78F36' }, + TaskDefinition: 'TD', Overrides: { ContainerOverrides: [ { @@ -128,7 +128,25 @@ test('Running a Fargate Task', () => { { Action: 'ecs:RunTask', Effect: 'Allow', - Resource: { Ref: 'TD49C78F36' }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { 'Fn::Select': [1, { 'Fn::Split': [':', { 'Ref': 'TD49C78F36' }] }] }, + ':', + { 'Fn::Select': [2, { 'Fn::Split': [':', { 'Ref': 'TD49C78F36' }] }] }, + ':', + { 'Fn::Select': [3, { 'Fn::Split': [':', { 'Ref': 'TD49C78F36' }] }] }, + ':', + { 'Fn::Select': [4, { 'Fn::Split': [':', { 'Ref': 'TD49C78F36' }] }] }, + ':', + { 'Fn::Select': [0, { 'Fn::Split': ['/', { 'Fn::Select': [5, { 'Fn::Split': [':', { 'Ref': 'TD49C78F36' }] }] }] }] }, + '/', + { 'Fn::Select': [1, { 'Fn::Split': ['/', { 'Fn::Select': [5, { 'Fn::Split': [':', { 'Ref': 'TD49C78F36' }] }] }] }] }, + ], + ], + }, }, { Action: ['ecs:StopTask', 'ecs:DescribeTasks'], @@ -196,7 +214,7 @@ test('Running an EC2 Task with bridge network', () => { Parameters: { Cluster: { 'Fn::GetAtt': ['ClusterEB0386A7', 'Arn'] }, LaunchType: 'EC2', - TaskDefinition: { Ref: 'TD49C78F36' }, + TaskDefinition: 'TD', Overrides: { ContainerOverrides: [ { @@ -232,7 +250,25 @@ test('Running an EC2 Task with bridge network', () => { { Action: 'ecs:RunTask', Effect: 'Allow', - Resource: { Ref: 'TD49C78F36' }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { 'Fn::Select': [1, { 'Fn::Split': [':', { 'Ref': 'TD49C78F36' }] }] }, + ':', + { 'Fn::Select': [2, { 'Fn::Split': [':', { 'Ref': 'TD49C78F36' }] }] }, + ':', + { 'Fn::Select': [3, { 'Fn::Split': [':', { 'Ref': 'TD49C78F36' }] }] }, + ':', + { 'Fn::Select': [4, { 'Fn::Split': [':', { 'Ref': 'TD49C78F36' }] }] }, + ':', + { 'Fn::Select': [0, { 'Fn::Split': ['/', { 'Fn::Select': [5, { 'Fn::Split': [':', { 'Ref': 'TD49C78F36' }] }] }] }] }, + '/', + { 'Fn::Select': [1, { 'Fn::Split': ['/', { 'Fn::Select': [5, { 'Fn::Split': [':', { 'Ref': 'TD49C78F36' }] }] }] }] }, + ], + ], + }, }, { Action: ['ecs:StopTask', 'ecs:DescribeTasks'], @@ -297,7 +333,7 @@ test('Running an EC2 Task with placement strategies', () => { Parameters: { Cluster: { 'Fn::GetAtt': ['ClusterEB0386A7', 'Arn'] }, LaunchType: 'EC2', - TaskDefinition: { Ref: 'TD49C78F36' }, + TaskDefinition: 'TD', PlacementConstraints: [{ Type: 'memberOf', Expression: 'blieptuut' }], PlacementStrategy: [{ Field: 'instanceId', Type: 'spread' }, { Field: 'cpu', Type: 'binpack' }, { Type: 'random' }], }, @@ -348,7 +384,7 @@ test('Running an EC2 Task with overridden number values', () => { Parameters: { Cluster: { 'Fn::GetAtt': ['ClusterEB0386A7', 'Arn'] }, LaunchType: 'EC2', - TaskDefinition: { Ref: 'TD49C78F36' }, + TaskDefinition: 'TD', Overrides: { ContainerOverrides: [ { diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index 1fb164cc6e8e2..8682de8dd13a9 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -84,9 +84,9 @@ definition. The definition is specified by its start state, and encompasses all states reachable from the start state: ```ts -const startState = new stepfunctions.Pass(this, 'StartState'); +const startState = new sfn.Pass(this, 'StartState'); -new stepfunctions.StateMachine(this, 'StateMachine', { +new sfn.StateMachine(this, 'StateMachine', { definition: startState }); ``` @@ -138,8 +138,8 @@ will be passed as the state's output. ```ts // Makes the current JSON state { ..., "subObject": { "hello": "world" } } -const pass = new stepfunctions.Pass(this, 'Add Hello World', { - result: stepfunctions.Result.fromObject({ hello: 'world' }), +const pass = new sfn.Pass(this, 'Add Hello World', { + result: sfn.Result.fromObject({ hello: 'world' }), resultPath: '$.subObject', }); @@ -154,9 +154,9 @@ The following example filters the `greeting` field from the state input and also injects a field called `otherData`. ```ts -const pass = new stepfunctions.Pass(this, 'Filter input and inject data', { +const pass = new sfn.Pass(this, 'Filter input and inject data', { parameters: { // input to the pass state - input: stepfunctions.JsonPath.stringAt('$.input.greeting'), + input: sfn.JsonPath.stringAt('$.input.greeting'), otherData: 'some-extra-stuff' }, }); @@ -177,8 +177,8 @@ state. ```ts // Wait until it's the time mentioned in the the state object's "triggerTime" // field. -const wait = new stepfunctions.Wait(this, 'Wait For Trigger Time', { - time: stepfunctions.WaitTime.timestampPath('$.triggerTime'), +const wait = new sfn.Wait(this, 'Wait For Trigger Time', { + time: sfn.WaitTime.timestampPath('$.triggerTime'), }); // Set the next state @@ -191,11 +191,11 @@ A `Choice` state can take a different path through the workflow based on the values in the execution's JSON state: ```ts -const choice = new stepfunctions.Choice(this, 'Did it work?'); +const choice = new sfn.Choice(this, 'Did it work?'); // Add conditions with .when() -choice.when(stepfunctions.Condition.stringEqual('$.status', 'SUCCESS'), successState); -choice.when(stepfunctions.Condition.numberGreaterThan('$.attempts', 5), failureState); +choice.when(sfn.Condition.stringEquals('$.status', 'SUCCESS'), successState); +choice.when(sfn.Condition.numberGreaterThan('$.attempts', 5), failureState); // Use .otherwise() to indicate what should be done if none of the conditions match choice.otherwise(tryAgainState); @@ -206,9 +206,9 @@ all branches come together and continuing as one (similar to how an `if ... then ... else` works in a programming language), use the `.afterwards()` method: ```ts -const choice = new stepfunctions.Choice(this, 'What color is it?'); -choice.when(stepfunctions.Condition.stringEqual('$.color', 'BLUE'), handleBlueItem); -choice.when(stepfunctions.Condition.stringEqual('$.color', 'RED'), handleRedItem); +const choice = new sfn.Choice(this, 'What color is it?'); +choice.when(sfn.Condition.stringEquals('$.color', 'BLUE'), handleBlueItem); +choice.when(sfn.Condition.stringEquals('$.color', 'RED'), handleRedItem); choice.otherwise(handleOtherItemColor); // Use .afterwards() to join all possible paths back together and continue @@ -275,7 +275,7 @@ A `Parallel` state executes one or more subworkflows in parallel. It can also be used to catch and recover from errors in subworkflows. ```ts -const parallel = new stepfunctions.Parallel(this, 'Do the work in parallel'); +const parallel = new sfn.Parallel(this, 'Do the work in parallel'); // Add branches to be executed in parallel parallel.branch(shipItem); @@ -298,7 +298,7 @@ Reaching a `Succeed` state terminates the state machine execution with a succesful status. ```ts -const success = new stepfunctions.Succeed(this, 'We did it!'); +const success = new sfn.Succeed(this, 'We did it!'); ``` ### Fail @@ -308,7 +308,7 @@ failure status. The fail state should report the reason for the failure. Failures can be caught by encompassing `Parallel` states. ```ts -const success = new stepfunctions.Fail(this, 'Fail', { +const success = new sfn.Fail(this, 'Fail', { error: 'WorkflowFailure', cause: "Something went wrong" }); @@ -323,11 +323,11 @@ While the `Parallel` state executes multiple branches of steps using the same in execute the same steps for multiple entries of an array in the state input. ```ts -const map = new stepfunctions.Map(this, 'Map State', { +const map = new sfn.Map(this, 'Map State', { maxConcurrency: 1, - itemsPath: stepfunctions.JsonPath.stringAt('$.inputForMap') + itemsPath: sfn.JsonPath.stringAt('$.inputForMap') }); -map.iterator(new stepfunctions.Pass(this, 'Pass State')); +map.iterator(new sfn.Pass(this, 'Pass State')); ``` ### Custom State @@ -397,7 +397,7 @@ const sm = new sfn.StateMachine(this, 'StateMachine', { }); // don't forget permissions. You need to assign them -table.grantWriteData(sm.role); +table.grantWriteData(sm); ``` ## Task Chaining @@ -420,7 +420,7 @@ const definition = step1 .branch(step9.next(step10))) .next(finish); -new stepfunctions.StateMachine(this, 'StateMachine', { +new sfn.StateMachine(this, 'StateMachine', { definition, }); ``` @@ -429,14 +429,13 @@ If you don't like the visual look of starting a chain directly off the first step, you can use `Chain.start`: ```ts -const definition = stepfunctions.Chain +const definition = sfn.Chain .start(step1) .next(step2) .next(step3) // ... ``` - ## State Machine Fragments It is possible to define reusable (or abstracted) mini-state machines by @@ -461,16 +460,16 @@ interface MyJobProps { jobFlavor: string; } -class MyJob extends stepfunctions.StateMachineFragment { - public readonly startState: State; - public readonly endStates: INextable[]; +class MyJob extends sfn.StateMachineFragment { + public readonly startState: sfn.State; + public readonly endStates: sfn.INextable[]; constructor(parent: cdk.Construct, id: string, props: MyJobProps) { super(parent, id); - const first = new stepfunctions.Task(this, 'First', { ... }); + const first = new sfn.Task(this, 'First', { ... }); // ... - const last = new stepfunctions.Task(this, 'Last', { ... }); + const last = new sfn.Task(this, 'Last', { ... }); this.startState = first; this.endStates = [last]; @@ -478,7 +477,7 @@ class MyJob extends stepfunctions.StateMachineFragment { } // Do 3 different variants of MyJob in parallel -new stepfunctions.Parallel(this, 'All jobs') +new sfn.Parallel(this, 'All jobs') .branch(new MyJob(this, 'Quick', { jobFlavor: 'quick' }).prefixStates()) .branch(new MyJob(this, 'Medium', { jobFlavor: 'medium' }).prefixStates()) .branch(new MyJob(this, 'Slow', { jobFlavor: 'slow' }).prefixStates()); @@ -500,7 +499,7 @@ You need the ARN to do so, so if you use Activities be sure to pass the Activity ARN into your worker pool: ```ts -const activity = new stepfunctions.Activity(this, 'Activity'); +const activity = new sfn.Activity(this, 'Activity'); // Read this CloudFormation Output from your application and use it to poll for work on // the activity. @@ -512,7 +511,7 @@ new cdk.CfnOutput(this, 'ActivityArn', { value: activity.activityArn }); Granting IAM permissions to an activity can be achieved by calling the `grant(principal, actions)` API: ```ts -const activity = new stepfunctions.Activity(this, 'Activity'); +const activity = new sfn.Activity(this, 'Activity'); const role = new iam.Role(stack, 'Role', { assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), @@ -564,11 +563,11 @@ destination LogGroup: ```ts const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); -new stepfunctions.StateMachine(stack, 'MyStateMachine', { - definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, 'Pass')), +new sfn.StateMachine(stack, 'MyStateMachine', { + definition: sfn.Chain.start(new sfn.Pass(stack, 'Pass')), logs: { destination: logGroup, - level: stepfunctions.LogLevel.ALL, + level: sfn.LogLevel.ALL, } }); ``` @@ -580,8 +579,8 @@ Enable X-Ray tracing for StateMachine: ```ts const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); -new stepfunctions.StateMachine(stack, 'MyStateMachine', { - definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, 'Pass')), +new sfn.StateMachine(stack, 'MyStateMachine', { + definition: sfn.Chain.start(new sfn.Pass(stack, 'Pass')), tracingEnabled: true }); ``` diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts index 56b14e69fa5d8..cced1d4519660 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/state-machine.ts @@ -140,12 +140,18 @@ abstract class StateMachineBase extends Resource implements IStateMachine { public static fromStateMachineArn(scope: Construct, id: string, stateMachineArn: string): IStateMachine { class Import extends StateMachineBase { public readonly stateMachineArn = stateMachineArn; + public readonly grantPrincipal = new iam.UnknownPrincipal({ resource: this }); } return new Import(scope, id); } public abstract readonly stateMachineArn: string; + /** + * The principal this state machine is running as + */ + public abstract readonly grantPrincipal: iam.IPrincipal; + /** * Grant the given identity permissions to start an execution of this state * machine. @@ -380,11 +386,11 @@ export class StateMachine extends StateMachineBase { const graph = new StateGraph(props.definition.startState, `State Machine ${id} definition`); graph.timeout = props.timeout; - this.stateMachineType = props.stateMachineType ? props.stateMachineType : StateMachineType.STANDARD; + this.stateMachineType = props.stateMachineType ?? StateMachineType.STANDARD; const resource = new CfnStateMachine(this, 'Resource', { stateMachineName: this.physicalName, - stateMachineType: props.stateMachineType ? props.stateMachineType : undefined, + stateMachineType: props.stateMachineType ?? undefined, roleArn: this.role.roleArn, definitionString: Stack.of(this).toJsonString(graph.toGraphJson()), loggingConfiguration: props.logs ? this.buildLoggingConfiguration(props.logs) : undefined, @@ -406,6 +412,13 @@ export class StateMachine extends StateMachineBase { }); } + /** + * The principal this state machine is running as + */ + public get grantPrincipal() { + return this.role.grantPrincipal; + } + /** * Add the given statement to the role's policy */ @@ -461,7 +474,7 @@ export class StateMachine extends StateMachineBase { /** * A State Machine */ -export interface IStateMachine extends IResource { +export interface IStateMachine extends IResource, iam.IGrantable { /** * The ARN of the state machine * @attribute diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts index de9b4d16aab01..fb5e52d2831b2 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -145,7 +145,7 @@ export class Pass extends State implements INextable { return { Type: StateType.PASS, Comment: this.comment, - Result: this.result ? this.result.value : undefined, + Result: this.result?.value, ResultPath: renderJsonPath(this.resultPath), ...this.renderInputOutput(), ...this.renderParameters(), diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts index 02cceb6e64ed2..42a4c8e9bf1b5 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts @@ -254,7 +254,7 @@ export abstract class State extends CoreConstruct implements IChainable { this.retries.push({ ...props, - errors: props.errors ? props.errors : [Errors.ALL], + errors: props.errors ?? [Errors.ALL], }); } @@ -268,7 +268,7 @@ export abstract class State extends CoreConstruct implements IChainable { this.catches.push({ next: handler, props: { - errors: props.errors ? props.errors : [Errors.ALL], + errors: props.errors ?? [Errors.ALL], resultPath: props.resultPath, }, }); @@ -352,7 +352,7 @@ export abstract class State extends CoreConstruct implements IChainable { protected renderChoices(): any { return { Choices: renderList(this.choices, renderChoice), - Default: this.defaultChoice ? this.defaultChoice.stateId : undefined, + Default: this.defaultChoice?.stateId, }; } diff --git a/packages/@aws-cdk/aws-stepfunctions/package.json b/packages/@aws-cdk/aws-stepfunctions/package.json index 2e9f998e1175f..8d149e097da10 100644 --- a/packages/@aws-cdk/aws-stepfunctions/package.json +++ b/packages/@aws-cdk/aws-stepfunctions/package.json @@ -82,6 +82,7 @@ "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.2.0" }, @@ -91,6 +92,7 @@ "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.2.0" }, diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts index 33353447abeec..e721b460a5358 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine.test.ts @@ -1,5 +1,6 @@ import '@aws-cdk/assert/jest'; import * as logs from '@aws-cdk/aws-logs'; +import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import * as stepfunctions from '../lib'; @@ -160,4 +161,61 @@ describe('State Machine', () => { ], }); }); + + test('grant access', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const sm = new stepfunctions.StateMachine(stack, 'MyStateMachine', { + definition: stepfunctions.Chain.start(new stepfunctions.Pass(stack, 'Pass')), + }); + const bucket = new s3.Bucket(stack, 'MyBucket'); + bucket.grantRead(sm); + + // THEN + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': [ + 'MyBucketF68F3FF0', + 'Arn', + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'MyBucketF68F3FF0', + 'Arn', + ], + }, + '/*', + ], + ], + }, + ], + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyStateMachineRoleDefaultPolicyE468EB18', + Roles: [ + { + Ref: 'MyStateMachineRoleD59FFEBC', + }, + ], + }); + }); }); diff --git a/packages/@aws-cdk/cdk-assets-schema/package.json b/packages/@aws-cdk/cdk-assets-schema/package.json index 897d8cdc829c8..08b52ca9c331d 100644 --- a/packages/@aws-cdk/cdk-assets-schema/package.json +++ b/packages/@aws-cdk/cdk-assets-schema/package.json @@ -50,7 +50,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0" diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index bf8b43b6865e2..2d117eebb4978 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,87 @@ +# CloudFormation Resource Specification v26.0.0 + +## New Resource Types + +* AWS::LookoutVision::Project +* AWS::SageMaker::FeatureGroup + +## Attribute Changes + +* AWS::MediaConnect::FlowVpcInterface FlowArn (__deleted__) +* AWS::MediaConnect::FlowVpcInterface Name (__deleted__) +* AWS::S3::AccessPoint NetworkOrigin (__added__) +* AWS::S3::AccessPoint PolicyStatus (__added__) + +## Property Changes + +* AWS::ACMPCA::Certificate ApiPassthrough (__added__) +* AWS::ACMPCA::Certificate ValidityNotBefore (__added__) +* AWS::AmazonMQ::Configuration AuthenticationStrategy (__added__) +* AWS::ApiGatewayV2::Stage AccessPolicyId (__added__) +* AWS::ECS::Cluster Configuration (__added__) +* AWS::Kinesis::Stream Tags.DuplicatesAllowed (__changed__) + * Old: false + * New: true +* AWS::MediaConnect::FlowVpcInterface FlowArn (__added__) +* AWS::MediaConnect::FlowVpcInterface Name (__added__) +* AWS::S3::AccessPoint NetworkOrigin (__deleted__) +* AWS::S3::AccessPoint PolicyStatus (__deleted__) +* AWS::SSM::MaintenanceWindowTask MaxConcurrency.Required (__changed__) + * Old: true + * New: false +* AWS::SSM::MaintenanceWindowTask MaxErrors.Required (__changed__) + * Old: true + * New: false +* AWS::SSM::MaintenanceWindowTask Targets.Required (__changed__) + * Old: true + * New: false +* AWS::SSO::InstanceAccessControlAttributeConfiguration InstanceAccessControlAttributeConfiguration (__deleted__) +* AWS::SageMaker::Device Tags.ItemType (__changed__) + * Old: Json + * New: Tag +* AWS::SageMaker::Device Tags.Type (__changed__) + * Old: Tag + * New: List +* AWS::SageMaker::DeviceFleet Tags.ItemType (__changed__) + * Old: Json + * New: Tag +* AWS::SageMaker::DeviceFleet Tags.Type (__changed__) + * Old: Tag + * New: List +* AWS::SageMaker::Model InferenceExecutionConfig (__added__) + +## Property Type Changes + +* AWS::ACMPCA::Certificate.ApiPassthrough (__added__) +* AWS::ACMPCA::Certificate.CertificatePolicyList (__added__) +* AWS::ACMPCA::Certificate.EdiPartyName (__added__) +* AWS::ACMPCA::Certificate.ExtendedKeyUsage (__added__) +* AWS::ACMPCA::Certificate.ExtendedKeyUsageList (__added__) +* AWS::ACMPCA::Certificate.Extensions (__added__) +* AWS::ACMPCA::Certificate.GeneralName (__added__) +* AWS::ACMPCA::Certificate.GeneralNameList (__added__) +* AWS::ACMPCA::Certificate.KeyUsage (__added__) +* AWS::ACMPCA::Certificate.OtherName (__added__) +* AWS::ACMPCA::Certificate.PolicyInformation (__added__) +* AWS::ACMPCA::Certificate.PolicyQualifierInfo (__added__) +* AWS::ACMPCA::Certificate.PolicyQualifierInfoList (__added__) +* AWS::ACMPCA::Certificate.Qualifier (__added__) +* AWS::ACMPCA::Certificate.Subject (__added__) +* AWS::AppFlow::Flow.IdFieldNamesList (__added__) +* AWS::ECS::Cluster.ClusterConfiguration (__added__) +* AWS::ECS::Cluster.ExecuteCommandConfiguration (__added__) +* AWS::ECS::Cluster.ExecuteCommandLogConfiguration (__added__) +* AWS::SageMaker::Model.InferenceExecutionConfig (__added__) +* AWS::AppFlow::Flow.SalesforceDestinationProperties IdFieldNames (__added__) +* AWS::AppFlow::Flow.SalesforceDestinationProperties WriteOperationType (__added__) +* AWS::DLM::LifecyclePolicy.CreateRule Location (__added__) +* AWS::DLM::LifecyclePolicy.CrossRegionCopyRule Target (__added__) +* AWS::DLM::LifecyclePolicy.CrossRegionCopyRule TargetRegion.Required (__changed__) + * Old: true + * New: false +* AWS::DLM::LifecyclePolicy.PolicyDetails ResourceLocations (__added__) + + # CloudFormation Resource Specification v24.0.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index 51105aade994f..1e212a919f492 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -24.0.0 +26.0.0 diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index 909704a8ddc33..9391cf7efc80d 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -1,5 +1,396 @@ { "PropertyTypes": { + "AWS::ACMPCA::Certificate.ApiPassthrough": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-apipassthrough.html", + "Properties": { + "Extensions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-apipassthrough.html#cfn-acmpca-certificate-apipassthrough-extensions", + "Required": false, + "Type": "Extensions", + "UpdateType": "Immutable" + }, + "Subject": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-apipassthrough.html#cfn-acmpca-certificate-apipassthrough-subject", + "Required": false, + "Type": "Subject", + "UpdateType": "Immutable" + } + } + }, + "AWS::ACMPCA::Certificate.CertificatePolicyList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-certificatepolicylist.html", + "Properties": { + "CertificatePolicyList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-certificatepolicylist.html#cfn-acmpca-certificate-certificatepolicylist-certificatepolicylist", + "ItemType": "PolicyInformation", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + } + } + }, + "AWS::ACMPCA::Certificate.EdiPartyName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-edipartyname.html", + "Properties": { + "NameAssigner": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-edipartyname.html#cfn-acmpca-certificate-edipartyname-nameassigner", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "PartyName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-edipartyname.html#cfn-acmpca-certificate-edipartyname-partyname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::ACMPCA::Certificate.ExtendedKeyUsage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-extendedkeyusage.html", + "Properties": { + "ExtendedKeyUsageObjectIdentifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-extendedkeyusage.html#cfn-acmpca-certificate-extendedkeyusage-extendedkeyusageobjectidentifier", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "ExtendedKeyUsageType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-extendedkeyusage.html#cfn-acmpca-certificate-extendedkeyusage-extendedkeyusagetype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::ACMPCA::Certificate.ExtendedKeyUsageList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-extendedkeyusagelist.html", + "Properties": { + "ExtendedKeyUsageList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-extendedkeyusagelist.html#cfn-acmpca-certificate-extendedkeyusagelist-extendedkeyusagelist", + "ItemType": "ExtendedKeyUsage", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + } + } + }, + "AWS::ACMPCA::Certificate.Extensions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-extensions.html", + "Properties": { + "CertificatePolicies": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-extensions.html#cfn-acmpca-certificate-extensions-certificatepolicies", + "Required": false, + "Type": "CertificatePolicyList", + "UpdateType": "Immutable" + }, + "ExtendedKeyUsage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-extensions.html#cfn-acmpca-certificate-extensions-extendedkeyusage", + "Required": false, + "Type": "ExtendedKeyUsageList", + "UpdateType": "Immutable" + }, + "KeyUsage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-extensions.html#cfn-acmpca-certificate-extensions-keyusage", + "Required": false, + "Type": "KeyUsage", + "UpdateType": "Immutable" + }, + "SubjectAlternativeNames": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-extensions.html#cfn-acmpca-certificate-extensions-subjectalternativenames", + "Required": false, + "Type": "GeneralNameList", + "UpdateType": "Immutable" + } + } + }, + "AWS::ACMPCA::Certificate.GeneralName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-generalname.html", + "Properties": { + "DirectoryName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-generalname.html#cfn-acmpca-certificate-generalname-directoryname", + "Required": false, + "Type": "Subject", + "UpdateType": "Immutable" + }, + "DnsName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-generalname.html#cfn-acmpca-certificate-generalname-dnsname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "EdiPartyName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-generalname.html#cfn-acmpca-certificate-generalname-edipartyname", + "Required": false, + "Type": "EdiPartyName", + "UpdateType": "Immutable" + }, + "IpAddress": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-generalname.html#cfn-acmpca-certificate-generalname-ipaddress", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "OtherName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-generalname.html#cfn-acmpca-certificate-generalname-othername", + "Required": false, + "Type": "OtherName", + "UpdateType": "Immutable" + }, + "RegisteredId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-generalname.html#cfn-acmpca-certificate-generalname-registeredid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Rfc822Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-generalname.html#cfn-acmpca-certificate-generalname-rfc822name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "UniformResourceIdentifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-generalname.html#cfn-acmpca-certificate-generalname-uniformresourceidentifier", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::ACMPCA::Certificate.GeneralNameList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-generalnamelist.html", + "Properties": { + "GeneralNameList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-generalnamelist.html#cfn-acmpca-certificate-generalnamelist-generalnamelist", + "ItemType": "GeneralName", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + } + } + }, + "AWS::ACMPCA::Certificate.KeyUsage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-keyusage.html", + "Properties": { + "CRLSign": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-keyusage.html#cfn-acmpca-certificate-keyusage-crlsign", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "DataEncipherment": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-keyusage.html#cfn-acmpca-certificate-keyusage-dataencipherment", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "DecipherOnly": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-keyusage.html#cfn-acmpca-certificate-keyusage-decipheronly", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "DigitalSignature": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-keyusage.html#cfn-acmpca-certificate-keyusage-digitalsignature", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "EncipherOnly": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-keyusage.html#cfn-acmpca-certificate-keyusage-encipheronly", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "KeyAgreement": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-keyusage.html#cfn-acmpca-certificate-keyusage-keyagreement", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "KeyCertSign": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-keyusage.html#cfn-acmpca-certificate-keyusage-keycertsign", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "KeyEncipherment": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-keyusage.html#cfn-acmpca-certificate-keyusage-keyencipherment", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "NonRepudiation": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-keyusage.html#cfn-acmpca-certificate-keyusage-nonrepudiation", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::ACMPCA::Certificate.OtherName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-othername.html", + "Properties": { + "TypeId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-othername.html#cfn-acmpca-certificate-othername-typeid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-othername.html#cfn-acmpca-certificate-othername-value", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::ACMPCA::Certificate.PolicyInformation": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-policyinformation.html", + "Properties": { + "CertPolicyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-policyinformation.html#cfn-acmpca-certificate-policyinformation-certpolicyid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "PolicyQualifiers": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-policyinformation.html#cfn-acmpca-certificate-policyinformation-policyqualifiers", + "Required": false, + "Type": "PolicyQualifierInfoList", + "UpdateType": "Immutable" + } + } + }, + "AWS::ACMPCA::Certificate.PolicyQualifierInfo": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-policyqualifierinfo.html", + "Properties": { + "PolicyQualifierId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-policyqualifierinfo.html#cfn-acmpca-certificate-policyqualifierinfo-policyqualifierid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Qualifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-policyqualifierinfo.html#cfn-acmpca-certificate-policyqualifierinfo-qualifier", + "Required": true, + "Type": "Qualifier", + "UpdateType": "Immutable" + } + } + }, + "AWS::ACMPCA::Certificate.PolicyQualifierInfoList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-policyqualifierinfolist.html", + "Properties": { + "PolicyQualifierInfoList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-policyqualifierinfolist.html#cfn-acmpca-certificate-policyqualifierinfolist-policyqualifierinfolist", + "ItemType": "PolicyQualifierInfo", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + } + } + }, + "AWS::ACMPCA::Certificate.Qualifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-qualifier.html", + "Properties": { + "CpsUri": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-qualifier.html#cfn-acmpca-certificate-qualifier-cpsuri", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::ACMPCA::Certificate.Subject": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html", + "Properties": { + "CommonName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html#cfn-acmpca-certificate-subject-commonname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Country": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html#cfn-acmpca-certificate-subject-country", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "DistinguishedNameQualifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html#cfn-acmpca-certificate-subject-distinguishednamequalifier", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "GenerationQualifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html#cfn-acmpca-certificate-subject-generationqualifier", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "GivenName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html#cfn-acmpca-certificate-subject-givenname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Initials": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html#cfn-acmpca-certificate-subject-initials", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Locality": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html#cfn-acmpca-certificate-subject-locality", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Organization": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html#cfn-acmpca-certificate-subject-organization", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "OrganizationalUnit": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html#cfn-acmpca-certificate-subject-organizationalunit", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Pseudonym": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html#cfn-acmpca-certificate-subject-pseudonym", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "SerialNumber": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html#cfn-acmpca-certificate-subject-serialnumber", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "State": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html#cfn-acmpca-certificate-subject-state", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Surname": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html#cfn-acmpca-certificate-subject-surname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Title": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-subject.html#cfn-acmpca-certificate-subject-title", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::ACMPCA::Certificate.Validity": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-validity.html", "Properties": { @@ -3033,6 +3424,18 @@ } } }, + "AWS::AppFlow::Flow.IdFieldNamesList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appflow-flow-idfieldnameslist.html", + "Properties": { + "IdFieldNamesList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appflow-flow-idfieldnameslist.html#cfn-appflow-flow-idfieldnameslist-idfieldnameslist", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::AppFlow::Flow.IncrementalPullConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appflow-flow-incrementalpullconfig.html", "Properties": { @@ -3184,11 +3587,23 @@ "Type": "ErrorHandlingConfig", "UpdateType": "Mutable" }, + "IdFieldNames": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appflow-flow-salesforcedestinationproperties.html#cfn-appflow-flow-salesforcedestinationproperties-idfieldnames", + "Required": false, + "Type": "IdFieldNamesList", + "UpdateType": "Mutable" + }, "Object": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appflow-flow-salesforcedestinationproperties.html#cfn-appflow-flow-salesforcedestinationproperties-object", "PrimitiveType": "String", "Required": true, "UpdateType": "Mutable" + }, + "WriteOperationType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appflow-flow-salesforcedestinationproperties.html#cfn-appflow-flow-salesforcedestinationproperties-writeoperationtype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -12981,6 +13396,12 @@ "Required": false, "UpdateType": "Mutable" }, + "Location": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-createrule.html#cfn-dlm-lifecyclepolicy-createrule-location", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Times": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-createrule.html#cfn-dlm-lifecyclepolicy-createrule-times", "PrimitiveItemType": "String", @@ -13057,10 +13478,16 @@ "Type": "CrossRegionCopyRetainRule", "UpdateType": "Mutable" }, + "Target": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-crossregioncopyrule.html#cfn-dlm-lifecyclepolicy-crossregioncopyrule-target", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "TargetRegion": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-crossregioncopyrule.html#cfn-dlm-lifecyclepolicy-crossregioncopyrule-targetregion", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" } } @@ -13198,6 +13625,13 @@ "Required": false, "UpdateType": "Mutable" }, + "ResourceLocations": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-policydetails.html#cfn-dlm-lifecyclepolicy-policydetails-resourcelocations", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "ResourceTypes": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-policydetails.html#cfn-dlm-lifecyclepolicy-policydetails-resourcetypes", "PrimitiveItemType": "String", @@ -18020,6 +18454,17 @@ } } }, + "AWS::ECS::Cluster.ClusterConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clusterconfiguration.html", + "Properties": { + "ExecuteCommandConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clusterconfiguration.html#cfn-ecs-cluster-clusterconfiguration-executecommandconfiguration", + "Required": false, + "Type": "ExecuteCommandConfiguration", + "UpdateType": "Mutable" + } + } + }, "AWS::ECS::Cluster.ClusterSettings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersettings.html", "Properties": { @@ -18037,6 +18482,64 @@ } } }, + "AWS::ECS::Cluster.ExecuteCommandConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandconfiguration.html", + "Properties": { + "KmsKeyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandconfiguration.html#cfn-ecs-cluster-executecommandconfiguration-kmskeyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "LogConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandconfiguration.html#cfn-ecs-cluster-executecommandconfiguration-logconfiguration", + "Required": false, + "Type": "ExecuteCommandLogConfiguration", + "UpdateType": "Mutable" + }, + "Logging": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandconfiguration.html#cfn-ecs-cluster-executecommandconfiguration-logging", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::ECS::Cluster.ExecuteCommandLogConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandlogconfiguration.html", + "Properties": { + "CloudWatchEncryptionEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandlogconfiguration.html#cfn-ecs-cluster-executecommandlogconfiguration-cloudwatchencryptionenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "CloudWatchLogGroupName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandlogconfiguration.html#cfn-ecs-cluster-executecommandlogconfiguration-cloudwatchloggroupname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "S3BucketName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandlogconfiguration.html#cfn-ecs-cluster-executecommandlogconfiguration-s3bucketname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "S3EncryptionEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandlogconfiguration.html#cfn-ecs-cluster-executecommandlogconfiguration-s3encryptionenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "S3KeyPrefix": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandlogconfiguration.html#cfn-ecs-cluster-executecommandlogconfiguration-s3keyprefix", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::ECS::Service.AwsVpcConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-service-awsvpcconfiguration.html", "Properties": { @@ -47726,6 +48229,23 @@ } } }, + "AWS::SageMaker::FeatureGroup.FeatureDefinition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-featuregroup-featuredefinition.html", + "Properties": { + "FeatureName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-featuregroup-featuredefinition.html#cfn-sagemaker-featuregroup-featuredefinition-featurename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "FeatureType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-featuregroup-featuredefinition.html#cfn-sagemaker-featuregroup-featuredefinition-featuretype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::SageMaker::Model.ContainerDefinition": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-model-containerdefinition.html", "Properties": { @@ -47790,6 +48310,17 @@ } } }, + "AWS::SageMaker::Model.InferenceExecutionConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-model-inferenceexecutionconfig.html", + "Properties": { + "Mode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-model-inferenceexecutionconfig.html#cfn-sagemaker-model-inferenceexecutionconfig-mode", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::SageMaker::Model.MultiModelConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-model-containerdefinition-multimodelconfig.html", "Properties": { @@ -51929,7 +52460,7 @@ } } }, - "ResourceSpecificationVersion": "24.0.0", + "ResourceSpecificationVersion": "26.0.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -51942,6 +52473,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-acmpca-certificate.html", "Properties": { + "ApiPassthrough": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-acmpca-certificate.html#cfn-acmpca-certificate-apipassthrough", + "Required": false, + "Type": "ApiPassthrough", + "UpdateType": "Immutable" + }, "CertificateAuthorityArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-acmpca-certificate.html#cfn-acmpca-certificate-certificateauthorityarn", "PrimitiveType": "String", @@ -51971,6 +52508,12 @@ "Required": true, "Type": "Validity", "UpdateType": "Immutable" + }, + "ValidityNotBefore": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-acmpca-certificate.html#cfn-acmpca-certificate-validitynotbefore", + "Required": false, + "Type": "Validity", + "UpdateType": "Immutable" } } }, @@ -52267,6 +52810,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amazonmq-configuration.html", "Properties": { + "AuthenticationStrategy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amazonmq-configuration.html#cfn-amazonmq-configuration-authenticationstrategy", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "Data": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-amazonmq-configuration.html#cfn-amazonmq-configuration-data", "PrimitiveType": "String", @@ -54041,6 +54590,12 @@ "Type": "AccessLogSettings", "UpdateType": "Mutable" }, + "AccessPolicyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-stage.html#cfn-apigatewayv2-stage-accesspolicyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "ApiId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-stage.html#cfn-apigatewayv2-stage-apiid", "PrimitiveType": "String", @@ -64723,6 +65278,12 @@ "Type": "List", "UpdateType": "Mutable" }, + "Configuration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-cluster.html#cfn-ecs-cluster-configuration", + "Required": false, + "Type": "ClusterConfiguration", + "UpdateType": "Mutable" + }, "DefaultCapacityProviderStrategy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-cluster.html#cfn-ecs-cluster-defaultcapacityproviderstrategy", "ItemType": "CapacityProviderStrategyItem", @@ -72208,7 +72769,7 @@ }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-kinesis-stream.html#cfn-kinesis-stream-tags", - "DuplicatesAllowed": false, + "DuplicatesAllowed": true, "ItemType": "Tag", "Required": false, "Type": "List", @@ -73334,6 +73895,22 @@ } } }, + "AWS::LookoutVision::Project": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lookoutvision-project.html", + "Properties": { + "ProjectName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lookoutvision-project.html#cfn-lookoutvision-project-projectname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::MSK::Cluster": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-msk-cluster.html", "Properties": { @@ -74013,12 +74590,6 @@ }, "AWS::MediaConnect::FlowVpcInterface": { "Attributes": { - "FlowArn": { - "PrimitiveType": "String" - }, - "Name": { - "PrimitiveType": "String" - }, "NetworkInterfaceIds": { "PrimitiveItemType": "String", "Type": "List" @@ -74026,6 +74597,18 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-mediaconnect-flowvpcinterface.html", "Properties": { + "FlowArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-mediaconnect-flowvpcinterface.html#cfn-mediaconnect-flowvpcinterface-flowarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-mediaconnect-flowvpcinterface.html#cfn-mediaconnect-flowvpcinterface-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, "RoleArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-mediaconnect-flowvpcinterface.html#cfn-mediaconnect-flowvpcinterface-rolearn", "PrimitiveType": "String", @@ -79615,6 +80198,14 @@ } }, "AWS::S3::AccessPoint": { + "Attributes": { + "NetworkOrigin": { + "PrimitiveType": "String" + }, + "PolicyStatus": { + "PrimitiveType": "Json" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-accesspoint.html", "Properties": { "Bucket": { @@ -79629,24 +80220,12 @@ "Required": false, "UpdateType": "Mutable" }, - "NetworkOrigin": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-accesspoint.html#cfn-s3-accesspoint-networkorigin", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, "Policy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-accesspoint.html#cfn-s3-accesspoint-policy", "PrimitiveType": "Json", "Required": false, "UpdateType": "Mutable" }, - "PolicyStatus": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-accesspoint.html#cfn-s3-accesspoint-policystatus", - "PrimitiveType": "Json", - "Required": false, - "UpdateType": "Mutable" - }, "PublicAccessBlockConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-accesspoint.html#cfn-s3-accesspoint-publicaccessblockconfiguration", "Required": false, @@ -80454,13 +81033,13 @@ "MaxConcurrency": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-maintenancewindowtask.html#cfn-ssm-maintenancewindowtask-maxconcurrency", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "MaxErrors": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-maintenancewindowtask.html#cfn-ssm-maintenancewindowtask-maxerrors", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Name": { @@ -80484,7 +81063,7 @@ "Targets": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-maintenancewindowtask.html#cfn-ssm-maintenancewindowtask-targets", "ItemType": "Target", - "Required": true, + "Required": false, "Type": "List", "UpdateType": "Mutable" }, @@ -80785,12 +81364,6 @@ "Type": "List", "UpdateType": "Mutable" }, - "InstanceAccessControlAttributeConfiguration": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sso-instanceaccesscontrolattributeconfiguration.html#cfn-sso-instanceaccesscontrolattributeconfiguration-instanceaccesscontrolattributeconfiguration", - "PrimitiveType": "Json", - "Required": false, - "UpdateType": "Mutable" - }, "InstanceArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sso-instanceaccesscontrolattributeconfiguration.html#cfn-sso-instanceaccesscontrolattributeconfiguration-instancearn", "PrimitiveType": "String", @@ -80973,9 +81546,9 @@ }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-device.html#cfn-sagemaker-device-tags", - "ItemType": "Json", + "ItemType": "Tag", "Required": false, - "Type": "Tag", + "Type": "List", "UpdateType": "Mutable" } } @@ -81009,9 +81582,9 @@ }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-devicefleet.html#cfn-sagemaker-devicefleet-tags", - "ItemType": "Json", + "ItemType": "Tag", "Required": false, - "Type": "Tag", + "Type": "List", "UpdateType": "Mutable" } } @@ -81106,6 +81679,69 @@ } } }, + "AWS::SageMaker::FeatureGroup": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-featuregroup.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-featuregroup.html#cfn-sagemaker-featuregroup-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "EventTimeFeatureName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-featuregroup.html#cfn-sagemaker-featuregroup-eventtimefeaturename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "FeatureDefinitions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-featuregroup.html#cfn-sagemaker-featuregroup-featuredefinitions", + "DuplicatesAllowed": true, + "ItemType": "FeatureDefinition", + "Required": true, + "Type": "List", + "UpdateType": "Immutable" + }, + "FeatureGroupName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-featuregroup.html#cfn-sagemaker-featuregroup-featuregroupname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "OfflineStoreConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-featuregroup.html#cfn-sagemaker-featuregroup-offlinestoreconfig", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Immutable" + }, + "OnlineStoreConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-featuregroup.html#cfn-sagemaker-featuregroup-onlinestoreconfig", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Immutable" + }, + "RecordIdentifierFeatureName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-featuregroup.html#cfn-sagemaker-featuregroup-recordidentifierfeaturename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "RoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-featuregroup.html#cfn-sagemaker-featuregroup-rolearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-featuregroup.html#cfn-sagemaker-featuregroup-tags", + "DuplicatesAllowed": true, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + } + } + }, "AWS::SageMaker::Model": { "Attributes": { "ModelName": { @@ -81133,6 +81769,12 @@ "Required": true, "UpdateType": "Immutable" }, + "InferenceExecutionConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-model.html#cfn-sagemaker-model-inferenceexecutionconfig", + "Required": false, + "Type": "InferenceExecutionConfig", + "UpdateType": "Immutable" + }, "ModelName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-model.html#cfn-sagemaker-model-modelname", "PrimitiveType": "String", diff --git a/packages/@aws-cdk/cloud-assembly-schema/NOTICE b/packages/@aws-cdk/cloud-assembly-schema/NOTICE index 17186448448a4..e2fc9a315f5ee 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/NOTICE +++ b/packages/@aws-cdk/cloud-assembly-schema/NOTICE @@ -28,6 +28,40 @@ SOFTWARE. ---------------- +** lru-cache - https://www.npmjs.com/package/lru-cache +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---------------- + +** yallist - https://www.npmjs.com/package/yallist +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---------------- + ** semver - https://www.npmjs.com/package/semver Copyright (c) Isaac Z. Schlueter and Contributors diff --git a/packages/@aws-cdk/cloud-assembly-schema/package.json b/packages/@aws-cdk/cloud-assembly-schema/package.json index 39c0254c1cc5d..6a47b8381a88c 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/package.json +++ b/packages/@aws-cdk/cloud-assembly-schema/package.json @@ -58,13 +58,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "@types/mock-fs": "^4.13.0", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "mock-fs": "^4.13.0", "pkglint": "0.0.0", - "typescript-json-schema": "^0.47.0" + "typescript-json-schema": "^0.49.0" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", @@ -89,7 +89,7 @@ }, "dependencies": { "jsonschema": "^1.4.0", - "semver": "^7.3.2" + "semver": "^7.3.4" }, "awscdkio": { "announce": false diff --git a/packages/@aws-cdk/cloudformation-diff/lib/diff/util.ts b/packages/@aws-cdk/cloudformation-diff/lib/diff/util.ts index 763afbecaf79e..f4c452499ee09 100644 --- a/packages/@aws-cdk/cloudformation-diff/lib/diff/util.ts +++ b/packages/@aws-cdk/cloudformation-diff/lib/diff/util.ts @@ -23,7 +23,8 @@ export function deepEqual(lvalue: any, rvalue: any): boolean { } // allows a numeric 10 and a literal "10" to be equivalent; // this is consistent with CloudFormation. - if (((typeof lvalue === 'string') || (typeof rvalue === 'string')) && (parseFloat(lvalue) === parseFloat(rvalue))) { + if ((typeof lvalue === 'string' || typeof rvalue === 'string') && + safeParseFloat(lvalue) === safeParseFloat(rvalue)) { return true; } if (typeof lvalue !== typeof rvalue) { return false; } @@ -132,3 +133,20 @@ export function unionOf(lv: string[] | Set, rv: string[] | Set): } return new Array(...result); } + +/** + * A parseFloat implementation that does the right thing for + * strings like '0.0.0' + * (for which JavaScript's parseFloat() returns 0). + */ +function safeParseFloat(str: string): number { + const ret = parseFloat(str); + if (ret === 0) { + // if the str is exactly '0', that's OK; + // but parseFloat() also returns 0 for things like '0.0'; + // in this case, return NaN, so we'll fall back to string comparison + return str === '0' ? ret : NaN; + } else { + return ret; + } +} diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index 089a802613048..c946e83bd59d3 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -29,14 +29,14 @@ "table": "^6.0.7" }, "devDependencies": { - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "@types/string-width": "^4.0.1", "@types/table": "^5.0.0", "cdk-build-tools": "0.0.0", - "fast-check": "^2.11.0", + "fast-check": "^2.12.1", "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.4.4" + "ts-jest": "^26.5.1" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", diff --git a/packages/@aws-cdk/cloudformation-diff/test/diff-template.test.ts b/packages/@aws-cdk/cloudformation-diff/test/diff-template.test.ts index 8110922a41790..bde05ef09056e 100644 --- a/packages/@aws-cdk/cloudformation-diff/test/diff-template.test.ts +++ b/packages/@aws-cdk/cloudformation-diff/test/diff-template.test.ts @@ -375,6 +375,34 @@ test('adding and removing quotes from a numeric property causes no changes', () expect(differences.resources.differenceCount).toBe(0); }); +test('versions are correctly detected as not numbers', () => { + const currentTemplate = { + Resources: { + ImageBuilderComponent: { + Type: 'AWS::ImageBuilder::Component', + Properties: { + Platform: 'Linux', + Version: '0.0.1', + }, + }, + }, + }; + const newTemplate = { + Resources: { + ImageBuilderComponent: { + Type: 'AWS::ImageBuilder::Component', + Properties: { + Platform: 'Linux', + Version: '0.0.2', + }, + }, + }, + }; + + const differences = diffTemplate(currentTemplate, newTemplate); + expect(differences.resources.differenceCount).toBe(1); +}); + test('single element arrays are equivalent to the single element in DependsOn expressions', () => { // GIVEN const currentTemplate = { diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index 6a3dd4c17346d..32f8524af52a3 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -159,6 +159,7 @@ "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-licensemanager": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-lookoutvision": "0.0.0", "@aws-cdk/aws-macie": "0.0.0", "@aws-cdk/aws-managedblockchain": "0.0.0", "@aws-cdk/aws-mediaconnect": "0.0.0", @@ -306,6 +307,7 @@ "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-licensemanager": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-lookoutvision": "0.0.0", "@aws-cdk/aws-macie": "0.0.0", "@aws-cdk/aws-managedblockchain": "0.0.0", "@aws-cdk/aws-mediaconnect": "0.0.0", @@ -360,12 +362,12 @@ }, "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.4.4" + "ts-jest": "^26.5.1" }, "keywords": [ "aws", diff --git a/packages/@aws-cdk/core/.vscode/tasks.json b/packages/@aws-cdk/core/.vscode/tasks.json new file mode 100644 index 0000000000000..3726d98503637 --- /dev/null +++ b/packages/@aws-cdk/core/.vscode/tasks.json @@ -0,0 +1,16 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": [ + "$tsc-watch" + ], + "group": "build", + "label": "npm: watch", + "detail": "cdk-watch", + "isBackground": true + } + ] +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/README.md b/packages/@aws-cdk/core/README.md index 8c5ce270d8dc1..880aea79a55fc 100644 --- a/packages/@aws-cdk/core/README.md +++ b/packages/@aws-cdk/core/README.md @@ -92,6 +92,65 @@ nested stack and referenced using `Fn::GetAtt "Outputs.Xxx"` from the parent. Nested stacks also support the use of Docker image and file assets. +## Accessing resources in a different stack + +You can access resources in a different stack, as long as they are in the +same account and AWS Region. The following example defines the stack `stack1`, +which defines an Amazon S3 bucket. Then it defines a second stack, `stack2`, +which takes the bucket from stack1 as a constructor property. + +```ts +const prod = { account: '123456789012', region: 'us-east-1' }; + +const stack1 = new StackThatProvidesABucket(app, 'Stack1' , { env: prod }); + +// stack2 will take a property { bucket: IBucket } +const stack2 = new StackThatExpectsABucket(app, 'Stack2', { + bucket: stack1.bucket, + env: prod +}); +``` + +If the AWS CDK determines that the resource is in the same account and +Region, but in a different stack, it automatically synthesizes AWS +CloudFormation +[Exports](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-exports.html) +in the producing stack and an +[Fn::ImportValue](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html) +in the consuming stack to transfer that information from one stack to the +other. + +### Removing automatic cross-stack references + +The automatic references created by CDK when you use resources across stacks +are convenient, but may block your deployments if you want to remove the +resources that are referenced in this way. You will see an error like: + +```text +Export Stack1:ExportsOutputFnGetAtt-****** cannot be deleted as it is in use by Stack1 +``` + +Let's say there is a Bucket in the `stack1`, and the `stack2` references its +`bucket.bucketName`. You now want to remove the bucket and run into the error above. + +It's not safe to remove `stack1.bucket` while `stack2` is still using it, so +unblocking yourself from this is a two-step process. This is how it works: + +DEPLOYMENT 1: break the relationship + +- Make sure `stack2` no longer references `bucket.bucketName` (maybe the consumer + stack now uses its own bucket, or it writes to an AWS DynamoDB table, or maybe you just + remove the Lambda Function altogether). +- In the `stack1` class, call `this.exportAttribute(this.bucket.bucketName)`. This + will make sure the CloudFormation Export continues to exist while the relationship + between the two stacks is being broken. +- Deploy (this will effectively only change the `stack2`, but it's safe to deploy both). + +DEPLOYMENT 2: remove the resource + +- You are now free to remove the `bucket` resource from `stack1`. +- Don't forget to remove the `exportAttribute()` call as well. +- Deploy again (this time only the `stack1` will be changed -- the bucket will be deleted). ## Durations diff --git a/packages/@aws-cdk/core/lib/app.ts b/packages/@aws-cdk/core/lib/app.ts index eb095f801ee4f..28e04607a0228 100644 --- a/packages/@aws-cdk/core/lib/app.ts +++ b/packages/@aws-cdk/core/lib/app.ts @@ -116,7 +116,7 @@ export class App extends Stage { this.node.setContext(cxapi.ANALYTICS_REPORTING_ENABLED_CONTEXT, analyticsReporting); } - const autoSynth = props.autoSynth !== undefined ? props.autoSynth : cxapi.OUTDIR_ENV in process.env; + const autoSynth = props.autoSynth ?? cxapi.OUTDIR_ENV in process.env; if (autoSynth) { // synth() guarantuees it will only execute once, so a default of 'true' // doesn't bite manual calling of the function. diff --git a/packages/@aws-cdk/core/lib/arn.ts b/packages/@aws-cdk/core/lib/arn.ts index 2ed3485a6c9b6..1ffcc2da2b33c 100644 --- a/packages/@aws-cdk/core/lib/arn.ts +++ b/packages/@aws-cdk/core/lib/arn.ts @@ -77,10 +77,10 @@ export class Arn { * can be 'undefined'. */ public static format(components: ArnComponents, stack: Stack): string { - const partition = components.partition !== undefined ? components.partition : stack.partition; - const region = components.region !== undefined ? components.region : stack.region; - const account = components.account !== undefined ? components.account : stack.account; - const sep = components.sep !== undefined ? components.sep : '/'; + const partition = components.partition ?? stack.partition; + const region = components.region ?? stack.region; + const account = components.account ?? stack.account; + const sep = components.sep ?? '/'; const values = ['arn', ':', partition, ':', components.service, ':', region, ':', account, ':', components.resource]; diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index c9a9c07e77f34..b1247fd913ea0 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -13,6 +13,17 @@ export interface BundlingOptions { */ readonly image: BundlingDockerImage; + /** + * The entrypoint to run in the Docker container. + * + * @example ['/bin/sh', '-c'] + * + * @see https://docs.docker.com/engine/reference/builder/#entrypoint + * + * @default - run the entrypoint defined in the image + */ + readonly entrypoint?: string[]; + /** * The command to run in the Docker container. * @@ -152,7 +163,15 @@ export class BundlingDockerImage { public run(options: DockerRunOptions = {}) { const volumes = options.volumes || []; const environment = options.environment || {}; - const command = options.command || []; + const entrypoint = options.entrypoint?.[0] || null; + const command = [ + ...options.entrypoint?.[1] + ? [...options.entrypoint.slice(1)] + : [], + ...options.command + ? [...options.command] + : [], + ]; const dockerArgs: string[] = [ 'run', '--rm', @@ -164,6 +183,9 @@ export class BundlingDockerImage { ...options.workingDirectory ? ['-w', options.workingDirectory] : [], + ...entrypoint + ? ['--entrypoint', entrypoint] + : [], this.image, ...command, ]; @@ -238,6 +260,13 @@ export enum DockerVolumeConsistency { * Docker run options */ export interface DockerRunOptions { + /** + * The entrypoint to run in the container. + * + * @default - run the entrypoint defined in the image + */ + readonly entrypoint?: string[]; + /** * The command to run in the container. * diff --git a/packages/@aws-cdk/core/lib/feature-flags.ts b/packages/@aws-cdk/core/lib/feature-flags.ts index 924283af30fcc..926a60168732f 100644 --- a/packages/@aws-cdk/core/lib/feature-flags.ts +++ b/packages/@aws-cdk/core/lib/feature-flags.ts @@ -24,6 +24,14 @@ export class FeatureFlags { * module. */ public isEnabled(featureFlag: string): boolean | undefined { - return this.construct.node.tryGetContext(featureFlag) ?? cxapi.futureFlagDefault(featureFlag); + const context = this.construct.node.tryGetContext(featureFlag); + if (cxapi.FUTURE_FLAGS_EXPIRED.includes(featureFlag)) { + if (context !== undefined) { + throw new Error(`Unsupported feature flag '${featureFlag}'. This flag existed on CDKv1 but has been removed in CDKv2.` + + ' CDK will now behave as the same as when the flag is enabled.'); + } + return true; + } + return context ?? cxapi.futureFlagDefault(featureFlag); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/core/lib/fs/copy.ts b/packages/@aws-cdk/core/lib/fs/copy.ts index b9feb555c8f65..627dc2aa988dc 100644 --- a/packages/@aws-cdk/core/lib/fs/copy.ts +++ b/packages/@aws-cdk/core/lib/fs/copy.ts @@ -5,7 +5,7 @@ import { CopyOptions, SymlinkFollowMode } from './options'; import { shouldFollow } from './utils'; export function copyDirectory(srcDir: string, destDir: string, options: CopyOptions = { }, rootDir?: string) { - const follow = options.follow !== undefined ? options.follow : SymlinkFollowMode.EXTERNAL; + const follow = options.follow ?? SymlinkFollowMode.EXTERNAL; rootDir = rootDir || srcDir; diff --git a/packages/@aws-cdk/core/lib/names.ts b/packages/@aws-cdk/core/lib/names.ts index 03998fcebe902..2d204c298d9fe 100644 --- a/packages/@aws-cdk/core/lib/names.ts +++ b/packages/@aws-cdk/core/lib/names.ts @@ -9,7 +9,7 @@ import { makeUniqueId } from './private/uniqueid'; export class Names { /** * Returns a CloudFormation-compatible unique identifier for a construct based - * on its path. The identifier includes a human readable porition rendered + * on its path. The identifier includes a human readable portion rendered * from the path components and a hash suffix. * * @param construct The construct @@ -23,7 +23,7 @@ export class Names { /** * Returns a CloudFormation-compatible unique identifier for a construct based - * on its path. The identifier includes a human readable porition rendered + * on its path. The identifier includes a human readable portion rendered * from the path components and a hash suffix. * * TODO (v2): replace with API to use `constructs.Node`. diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts index 46d44563b4a96..27618d6776f21 100644 --- a/packages/@aws-cdk/core/lib/private/refs.ts +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -1,22 +1,19 @@ // ---------------------------------------------------- // CROSS REFERENCES // ---------------------------------------------------- -import * as cxapi from '@aws-cdk/cx-api'; import { CfnElement } from '../cfn-element'; import { CfnOutput } from '../cfn-output'; import { CfnParameter } from '../cfn-parameter'; -import { Construct, IConstruct } from '../construct-compat'; -import { FeatureFlags } from '../feature-flags'; +import { IConstruct } from '../construct-compat'; import { Names } from '../names'; import { Reference } from '../reference'; import { IResolvable } from '../resolvable'; import { Stack } from '../stack'; -import { Token } from '../token'; +import { Token, Tokenization } from '../token'; import { CfnReference } from './cfn-reference'; import { Intrinsic } from './intrinsic'; import { findTokens } from './resolve'; -import { makeUniqueId } from './uniqueid'; /** * This is called from the App level to resolve all references defined. Each @@ -167,55 +164,10 @@ function findAllReferences(root: IConstruct) { function createImportValue(reference: Reference): Intrinsic { const exportingStack = Stack.of(reference.target); - // Ensure a singleton "Exports" scoping Construct - // This mostly exists to trigger LogicalID munging, which would be - // disabled if we parented constructs directly under Stack. - // Also it nicely prevents likely construct name clashes - const exportsScope = getCreateExportsScope(exportingStack); + const importExpr = exportingStack.exportValue(reference); - // Ensure a singleton CfnOutput for this value - const resolved = exportingStack.resolve(reference); - const id = 'Output' + JSON.stringify(resolved); - const exportName = generateExportName(exportsScope, id); - - if (Token.isUnresolved(exportName)) { - throw new Error(`unresolved token in generated export name: ${JSON.stringify(exportingStack.resolve(exportName))}`); - } - - const output = exportsScope.node.tryFindChild(id) as CfnOutput; - if (!output) { - new CfnOutput(exportsScope, id, { value: Token.asString(reference), exportName }); - } - - // We want to return an actual FnImportValue Token here, but Fn.importValue() returns a 'string', - // so construct one in-place. - return new Intrinsic({ 'Fn::ImportValue': exportName }); -} - -function getCreateExportsScope(stack: Stack) { - const exportsName = 'Exports'; - let stackExports = stack.node.tryFindChild(exportsName) as Construct; - if (stackExports === undefined) { - stackExports = new Construct(stack, exportsName); - } - - return stackExports; -} - -function generateExportName(stackExports: Construct, id: string) { - const stackRelativeExports = FeatureFlags.of(stackExports).isEnabled(cxapi.STACK_RELATIVE_EXPORTS_CONTEXT); - const stack = Stack.of(stackExports); - - const components = [ - ...stackExports.node.scopes - .slice(stackRelativeExports ? stack.node.scopes.length : 2) - .map(c => c.node.id), - id, - ]; - const prefix = stack.stackName ? stack.stackName + ':' : ''; - const localPart = makeUniqueId(components); - const maxLength = 255; - return prefix + localPart.slice(Math.max(0, localPart.length - maxLength + prefix.length)); + // I happen to know this returns a Fn.importValue() which implements Intrinsic. + return Tokenization.reverseCompleteString(importExpr) as Intrinsic; } // ------------------------------------------------------------------------------------------------ @@ -262,6 +214,25 @@ function createNestedStackOutput(producer: Stack, reference: Reference): CfnRefe return producer.nestedStackResource.getAtt(`Outputs.${output.logicalId}`) as CfnReference; } +/** + * Translate a Reference into a nested stack into a value in the parent stack + * + * Will create Outputs along the chain of Nested Stacks, and return the final `{ Fn::GetAtt }`. + */ +export function referenceNestedStackValueInParent(reference: Reference, targetStack: Stack) { + let currentStack = Stack.of(reference.target); + if (currentStack !== targetStack && !isNested(currentStack, targetStack)) { + throw new Error(`Referenced resource must be in stack '${targetStack.node.path}', got '${reference.target.node.path}'`); + } + + while (currentStack !== targetStack) { + reference = createNestedStackOutput(Stack.of(reference.target), reference); + currentStack = Stack.of(reference.target); + } + + return reference; +} + /** * @returns true if this stack is a direct or indirect parent of the nested * stack `nested`. @@ -282,4 +253,4 @@ function isNested(nested: Stack, parent: Stack): boolean { // recurse with the child's direct parent return isNested(nested.nestedStackParent, parent); -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/private/token-map.ts b/packages/@aws-cdk/core/lib/private/token-map.ts index 1b4ea48c04440..2523037724ef0 100644 --- a/packages/@aws-cdk/core/lib/private/token-map.ts +++ b/packages/@aws-cdk/core/lib/private/token-map.ts @@ -34,7 +34,34 @@ export class TokenMap { private readonly stringTokenMap = new Map(); private readonly numberTokenMap = new Map(); - private tokenCounter = 0; + + /** + * Counter to assign unique IDs to tokens + * + * Start at a random number to prevent people from accidentally taking + * dependencies on token values between runs. + * + * This is most prominent in tests, where people will write: + * + * ```ts + * sha256(JSON.stringify({ ...some structure that can contain tokens ... })) + * ``` + * + * This should have been: + * + * ```ts + * sha256(JSON.stringify(stack.resolve({ ...some structure that can contain tokens ... }))) + * ``` + * + * The hash value is hard to inspect for correctness. It will LOOK consistent + * during testing, but will break as soon as someone stringifies another + * token before the run. + * + * By changing the starting number for tokens, we ensure that the hash is almost + * guaranteed to be different during a few test runs, so the hashing of unresolved + * tokens can be detected. + */ + private tokenCounter = Math.floor(Math.random() * 10); /** * Generate a unique string for this Token, returning a key diff --git a/packages/@aws-cdk/core/lib/private/tree-metadata.ts b/packages/@aws-cdk/core/lib/private/tree-metadata.ts index eb18252a0e8bd..caa5c37a5940d 100644 --- a/packages/@aws-cdk/core/lib/private/tree-metadata.ts +++ b/packages/@aws-cdk/core/lib/private/tree-metadata.ts @@ -9,6 +9,13 @@ import { IInspectable, TreeInspector } from '../tree'; const FILE_PATH = 'tree.json'; +/** + * Symbol for accessing jsii runtime information + * + * Introduced in jsii 1.19.0, cdk 1.90.0. + */ +const JSII_RUNTIME_SYMBOL = Symbol.for('jsii.rtti'); + /** * Construct that is automatically attached to the top-level `App`. * This generates, as part of synthesis, a file containing the construct tree and the metadata for each node in the tree. @@ -41,11 +48,14 @@ export class TreeMetadata extends Construct { .filter((child) => child !== undefined) .reduce((map, child) => Object.assign(map, { [child!.id]: child }), {}); + const jsiiRuntimeInfo = Object.getPrototypeOf(construct).constructor[JSII_RUNTIME_SYMBOL]; + const node: Node = { id: construct.node.id || 'App', path: construct.node.path, children: Object.keys(childrenMap).length === 0 ? undefined : childrenMap, attributes: this.synthAttributes(construct), + constructInfo: constructInfoFromRuntimeInfo(jsiiRuntimeInfo), }; lookup[node.path] = node; @@ -86,9 +96,32 @@ export class TreeMetadata extends Construct { } } +function constructInfoFromRuntimeInfo(jsiiRuntimeInfo: any): ConstructInfo | undefined { + if (typeof jsiiRuntimeInfo === 'object' + && jsiiRuntimeInfo !== null + && typeof jsiiRuntimeInfo.fqn === 'string' + && typeof jsiiRuntimeInfo.version === 'string') { + return { fqn: jsiiRuntimeInfo.fqn, version: jsiiRuntimeInfo.version }; + } + return undefined; +} + interface Node { - id: string; - path: string; - children?: { [key: string]: Node }; - attributes?: { [key: string]: any }; + readonly id: string; + readonly path: string; + readonly children?: { [key: string]: Node }; + readonly attributes?: { [key: string]: any }; + + /** + * Information on the construct class that led to this node, if available + */ + readonly constructInfo?: ConstructInfo; +} + +/** + * Source information on a construct (class fqn and version) + */ +interface ConstructInfo { + readonly fqn: string; + readonly version: string; } diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts index 16ea69c1b2302..1a9a2ab8ee0cc 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts @@ -57,7 +57,6 @@ export class BootstraplessSynthesizer extends DefaultStackSynthesizer { this.emitStackArtifact(this.stack, session, { assumeRoleArn: this.deployRoleArn, cloudFormationExecutionRoleArn: this.cloudFormationExecutionRoleArn, - requiresBootstrapStackVersion: 1, }); } } diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts index b4f9f29cf732a..2527bca98c9d6 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts @@ -291,7 +291,8 @@ export class DefaultStackSynthesizer extends StackSynthesizer { assertBound(this.bucketName); validateFileAssetSource(asset); - const objectKey = this.bucketPrefix + asset.sourceHash + (asset.packaging === FileAssetPackaging.ZIP_DIRECTORY ? '.zip' : ''); + const extension = asset.fileName != undefined ? path.extname(asset.fileName) : ''; + const objectKey = this.bucketPrefix + asset.sourceHash + (asset.packaging === FileAssetPackaging.ZIP_DIRECTORY ? '.zip' : extension); // Add to manifest this.files[asset.sourceHash] = { @@ -448,7 +449,12 @@ export class DefaultStackSynthesizer extends StackSynthesizer { // // Instead, we'll have a protocol with the CLI that we put an 's3://.../...' URL here, and the CLI // is going to resolve it to the correct 'https://.../' URL before it gives it to CloudFormation. - return `s3://${this.bucketName}/${this.bucketPrefix}${sourceHash}`; + // + // ALSO: it would be great to reuse the return value of `addFileAsset()` here, except those contain + // CloudFormation REFERENCES to locations, not actual locations (can contain `{ Ref: AWS::Region }` and + // `{ Ref: SomeParameter }` etc). We therefore have to duplicate some logic here :(. + const extension = path.extname(this.stack.templateFile); + return `s3://${this.bucketName}/${this.bucketPrefix}${sourceHash}${extension}`; } /** diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index c6a8a56916f2f..2284ddedc203f 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -371,7 +371,7 @@ export class Stack extends CoreConstruct implements ITaggable { this.templateOptions.description = props.description; } - this._stackName = props.stackName !== undefined ? props.stackName : this.generateStackName(); + this._stackName = props.stackName ?? this.generateStackName(); this.tags = new TagManager(TagType.KEY_VALUE, 'aws:cdk:stack', props.tags); if (!VALID_STACK_NAME_REGEX.test(this.stackName)) { @@ -775,6 +775,93 @@ export class Stack extends CoreConstruct implements ITaggable { } } + /** + * Create a CloudFormation Export for a value + * + * Returns a string representing the corresponding `Fn.importValue()` + * expression for this Export. You can control the name for the export by + * passing the `name` option. + * + * If you don't supply a value for `name`, the value you're exporting must be + * a Resource attribute (for example: `bucket.bucketName`) and it will be + * given the same name as the automatic cross-stack reference that would be created + * if you used the attribute in another Stack. + * + * One of the uses for this method is to *remove* the relationship between + * two Stacks established by automatic cross-stack references. It will + * temporarily ensure that the CloudFormation Export still exists while you + * remove the reference from the consuming stack. After that, you can remove + * the resource and the manual export. + * + * ## Example + * + * Here is how the process works. Let's say there are two stacks, + * `producerStack` and `consumerStack`, and `producerStack` has a bucket + * called `bucket`, which is referenced by `consumerStack` (perhaps because + * an AWS Lambda Function writes into it, or something like that). + * + * It is not safe to remove `producerStack.bucket` because as the bucket is being + * deleted, `consumerStack` might still be using it. + * + * Instead, the process takes two deployments: + * + * ### Deployment 1: break the relationship + * + * - Make sure `consumerStack` no longer references `bucket.bucketName` (maybe the consumer + * stack now uses its own bucket, or it writes to an AWS DynamoDB table, or maybe you just + * remove the Lambda Function altogether). + * - In the `ProducerStack` class, call `this.exportValue(this.bucket.bucketName)`. This + * will make sure the CloudFormation Export continues to exist while the relationship + * between the two stacks is being broken. + * - Deploy (this will effectively only change the `consumerStack`, but it's safe to deploy both). + * + * ### Deployment 2: remove the bucket resource + * + * - You are now free to remove the `bucket` resource from `producerStack`. + * - Don't forget to remove the `exportValue()` call as well. + * - Deploy again (this time only the `producerStack` will be changed -- the bucket will be deleted). + */ + public exportValue(exportedValue: any, options: ExportValueOptions = {}) { + if (options.name) { + new CfnOutput(this, `Export${options.name}`, { + value: exportedValue, + exportName: options.name, + }); + return Fn.importValue(options.name); + } + + const resolvable = Tokenization.reverse(exportedValue); + if (!resolvable || !Reference.isReference(resolvable)) { + throw new Error('exportValue: either supply \'name\' or make sure to export a resource attribute (like \'bucket.bucketName\')'); + } + + // "teleport" the value here, in case it comes from a nested stack. This will also + // ensure the value is from our own scope. + const exportable = referenceNestedStackValueInParent(resolvable, this); + + // Ensure a singleton "Exports" scoping Construct + // This mostly exists to trigger LogicalID munging, which would be + // disabled if we parented constructs directly under Stack. + // Also it nicely prevents likely construct name clashes + const exportsScope = getCreateExportsScope(this); + + // Ensure a singleton CfnOutput for this value + const resolved = this.resolve(exportable); + const id = 'Output' + JSON.stringify(resolved); + const exportName = generateExportName(exportsScope, id); + + if (Token.isUnresolved(exportName)) { + throw new Error(`unresolved token in generated export name: ${JSON.stringify(this.resolve(exportName))}`); + } + + const output = exportsScope.node.tryFindChild(id) as CfnOutput; + if (!output) { + new CfnOutput(exportsScope, id, { value: Token.asString(exportable), exportName }); + } + + return Fn.importValue(exportName); + } + /** * Returns the naming scheme used to allocate logical IDs. By default, uses * the `HashedAddressingScheme` but this method can be overridden to customize @@ -1143,18 +1230,58 @@ function makeStackName(components: string[]) { return makeUniqueId(components); } +function getCreateExportsScope(stack: Stack) { + const exportsName = 'Exports'; + let stackExports = stack.node.tryFindChild(exportsName) as CoreConstruct; + if (stackExports === undefined) { + stackExports = new CoreConstruct(stack, exportsName); + } + + return stackExports; +} + +function generateExportName(stackExports: CoreConstruct, id: string) { + const stackRelativeExports = FeatureFlags.of(stackExports).isEnabled(cxapi.STACK_RELATIVE_EXPORTS_CONTEXT); + const stack = Stack.of(stackExports); + + const components = [ + ...stackExports.node.scopes + .slice(stackRelativeExports ? stack.node.scopes.length : 2) + .map(c => c.node.id), + id, + ]; + const prefix = stack.stackName ? stack.stackName + ':' : ''; + const localPart = makeUniqueId(components); + const maxLength = 255; + return prefix + localPart.slice(Math.max(0, localPart.length - maxLength + prefix.length)); +} + +interface StackDependency { + stack: Stack; + reasons: string[]; +} + +/** + * Options for the `stack.exportValue()` method + */ +export interface ExportValueOptions { + /** + * The name of the export to create + * + * @default - A name is automatically chosen + */ + readonly name?: string; +} + // These imports have to be at the end to prevent circular imports +import { CfnOutput } from './cfn-output'; import { addDependency } from './deps'; +import { FileSystem } from './fs'; +import { Names } from './names'; import { Reference } from './reference'; import { IResolvable } from './resolvable'; import { DefaultStackSynthesizer, IStackSynthesizer, LegacyStackSynthesizer } from './stack-synthesizers'; 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; - reasons: string[]; -} +import { Token, Tokenization } from './token'; +import { referenceNestedStackValueInParent } from './private/refs'; diff --git a/packages/@aws-cdk/core/lib/tag-aspect.ts b/packages/@aws-cdk/core/lib/tag-aspect.ts index d56c28b551d87..09d79c7ea9799 100644 --- a/packages/@aws-cdk/core/lib/tag-aspect.ts +++ b/packages/@aws-cdk/core/lib/tag-aspect.ts @@ -126,7 +126,7 @@ export class Tag extends TagBase { resource.tags.setTag( this.key, this.value, - this.props.priority !== undefined ? this.props.priority : this.defaultPriority, + this.props.priority ?? this.defaultPriority, this.props.applyToLaunchedInstances !== false, ); } @@ -175,7 +175,7 @@ export class RemoveTag extends TagBase { protected applyTag(resource: ITaggable): void { if (resource.tags.applyTagAspectHere(this.props.includeResourceTypes, this.props.excludeResourceTypes)) { - resource.tags.removeTag(this.key, this.props.priority !== undefined ? this.props.priority : this.defaultPriority); + resource.tags.removeTag(this.key, this.props.priority ?? this.defaultPriority); } } } diff --git a/packages/@aws-cdk/core/lib/token.ts b/packages/@aws-cdk/core/lib/token.ts index 5f98db7a4f11f..f92a2560cac7c 100644 --- a/packages/@aws-cdk/core/lib/token.ts +++ b/packages/@aws-cdk/core/lib/token.ts @@ -132,6 +132,19 @@ export class Tokenization { return TokenMap.instance().splitString(s); } + /** + * Un-encode a string which is either a complete encoded token, or doesn't contain tokens at all + * + * It's illegal for the string to be a concatenation of an encoded token and something else. + */ + public static reverseCompleteString(s: string): IResolvable | undefined { + const fragments = Tokenization.reverseString(s); + if (fragments.length !== 1) { + throw new Error(`Tokenzation.reverseCompleteString: argument must not be a concatentation, got '${s}'`); + } + return fragments.firstToken; + } + /** * Un-encode a Tokenized value from a number */ @@ -146,6 +159,19 @@ export class Tokenization { return TokenMap.instance().lookupList(l); } + /** + * Reverse any value into a Resolvable, if possible + * + * In case of a string, the string must not be a concatenation. + */ + public static reverse(x: any): IResolvable | undefined { + if (Tokenization.isResolvable(x)) { return x; } + if (typeof x === 'string') { return Tokenization.reverseCompleteString(x); } + if (Array.isArray(x)) { return Tokenization.reverseList(x); } + if (typeof x === 'number') { return Tokenization.reverseNumber(x); } + return undefined; + } + /** * Resolves an object by evaluating all tokens and removing any undefined or empty objects or arrays. * Values can only be primitives, arrays or tokens. Other objects (i.e. with methods) will be rejected. @@ -157,7 +183,7 @@ export class Tokenization { return resolve(obj, { scope: options.scope, resolver: options.resolver, - preparing: (options.preparing !== undefined ? options.preparing : false), + preparing: (options.preparing ?? false), }); } diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index d1245e62faf73..26fef1278c798 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -177,15 +177,15 @@ "@types/lodash": "^4.14.168", "@types/minimatch": "^3.0.3", "@types/node": "^10.17.51", - "@types/sinon": "^9.0.9", + "@types/sinon": "^9.0.10", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "fast-check": "^2.11.0", + "fast-check": "^2.12.1", "lodash": "^4.17.20", "nodeunit-shim": "0.0.0", "pkglint": "0.0.0", - "sinon": "^9.2.1", - "ts-mock-imports": "^1.3.1" + "sinon": "^9.2.4", + "ts-mock-imports": "^1.3.3" }, "dependencies": { "@aws-cdk/cloud-assembly-schema": "0.0.0", diff --git a/packages/@aws-cdk/core/test/bundling.test.ts b/packages/@aws-cdk/core/test/bundling.test.ts index e2b0d6b43b98b..258860d65585c 100644 --- a/packages/@aws-cdk/core/test/bundling.test.ts +++ b/packages/@aws-cdk/core/test/bundling.test.ts @@ -171,6 +171,44 @@ nodeunitShim({ test.done(); }, + 'custom entrypoint is passed through to docker exec'(test: Test) { + const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + const image = BundlingDockerImage.fromRegistry('alpine'); + image.run({ + entrypoint: ['/cool/entrypoint', '--cool-entrypoint-arg'], + command: ['cool', 'command'], + environment: { + VAR1: 'value1', + VAR2: 'value2', + }, + volumes: [{ hostPath: '/host-path', containerPath: '/container-path' }], + workingDirectory: '/working-directory', + user: 'user:group', + }); + + test.ok(spawnSyncStub.calledWith('docker', [ + 'run', '--rm', + '-u', 'user:group', + '-v', '/host-path:/container-path:delegated', + '--env', 'VAR1=value1', + '--env', 'VAR2=value2', + '-w', '/working-directory', + '--entrypoint', '/cool/entrypoint', + 'alpine', + '--cool-entrypoint-arg', + 'cool', 'command', + ], { stdio: ['ignore', process.stderr, 'inherit'] })); + test.done(); + }, + 'cp utility copies from an image'(test: Test) { // GIVEN const containerId = '1234567890abcdef1234567890abcdef'; diff --git a/packages/@aws-cdk/core/test/private/tree-metadata.test.ts b/packages/@aws-cdk/core/test/private/tree-metadata.test.ts index f62df683c422a..b7173aca585ec 100644 --- a/packages/@aws-cdk/core/test/private/tree-metadata.test.ts +++ b/packages/@aws-cdk/core/test/private/tree-metadata.test.ts @@ -2,10 +2,10 @@ import * as fs from 'fs'; import * as path from 'path'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { nodeunitShim, Test } from 'nodeunit-shim'; -import { App, CfnParameter, CfnResource, Construct, Lazy, Stack, TreeInspector } from '../../lib/index'; +import { App, CfnParameter, CfnResource, Construct as CfnConstruct, Lazy, Stack, TreeInspector } from '../../lib/index'; abstract class AbstractCfnResource extends CfnResource { - constructor(scope: Construct, id: string) { + constructor(scope: CfnConstruct, id: string) { super(scope, id, { type: 'CDK::UnitTest::MyCfnResource', }); @@ -24,7 +24,7 @@ nodeunitShim({ const app = new App(); const stack = new Stack(app, 'mystack'); - new Construct(stack, 'myconstruct'); + new CfnConstruct(stack, 'myconstruct'); const assembly = app.synth(); const treeArtifact = assembly.tree(); @@ -32,26 +32,26 @@ nodeunitShim({ test.deepEqual(readJson(assembly.directory, treeArtifact!.file), { version: 'tree-0.1', - tree: { + tree: expect.objectContaining({ id: 'App', path: '', children: { - Tree: { + Tree: expect.objectContaining({ id: 'Tree', path: 'Tree', - }, - mystack: { + }), + mystack: expect.objectContaining({ id: 'mystack', path: 'mystack', children: { - myconstruct: { + myconstruct: expect.objectContaining({ id: 'myconstruct', path: 'mystack/myconstruct', - }, + }), }, - }, + }), }, - }, + }), }); test.done(); }, @@ -80,19 +80,19 @@ nodeunitShim({ test.deepEqual(readJson(assembly.directory, treeArtifact!.file), { version: 'tree-0.1', - tree: { + tree: expect.objectContaining({ id: 'App', path: '', children: { - Tree: { + Tree: expect.objectContaining({ id: 'Tree', path: 'Tree', - }, - mystack: { + }), + mystack: expect.objectContaining({ id: 'mystack', path: 'mystack', children: { - mycfnresource: { + mycfnresource: expect.objectContaining({ id: 'mycfnresource', path: 'mystack/mycfnresource', attributes: { @@ -106,15 +106,72 @@ nodeunitShim({ }, }, }, - }, + }), }, - }, + }), }, - }, + }), }); test.done(); }, + 'tree metadata has construct class & version in there'(test: Test) { + // The runtime metadata this test relies on is only available if the most + // recent compile has happened using 'jsii', as the jsii compiler injects + // this metadata. + // + // If the most recent compile was using 'tsc', the metadata will not have + // been injected, and the test will fail. + // + // People may choose to run `tsc` directly (instead of `yarn build` for + // example) to escape the additional TSC compilation time that is necessary + // to run 'eslint', or the additional time that 'jsii' needs to analyze the + // type system), this test is allowed to fail if we're not running on CI. + // + // If the compile of this library has been done using `tsc`, the runtime + // information will always find `constructs.Construct` as the construct + // identifier, since `constructs` will have had a release build done using `jsii`. + // + // If this test is running on CodeBuild, we will require that the more specific + // class names are found. If this test is NOT running on CodeBuild, we will + // allow the specific class name (for a 'jsii' build) or the generic + // 'constructs.Construct' class name (for a 'tsc' build). + const app = new App(); + + const stack = new Stack(app, 'mystack'); + new CfnResource(stack, 'myconstruct', { type: 'Aws::Some::Resource' }); + + const assembly = app.synth(); + const treeArtifact = assembly.tree(); + test.ok(treeArtifact); + + const codeBuild = !!process.env.CODEBUILD_BUILD_ID; + + test.deepEqual(readJson(assembly.directory, treeArtifact!.file), { + version: 'tree-0.1', + tree: expect.objectContaining({ + children: expect.objectContaining({ + mystack: expect.objectContaining({ + constructInfo: { + fqn: expect.stringMatching(codeBuild ? /\bStack$/ : /\bStack$|^constructs.Construct$/), + version: expect.any(String), + }, + children: { + myconstruct: expect.objectContaining({ + constructInfo: { + fqn: expect.stringMatching(codeBuild ? /\bCfnResource$/ : /\bCfnResource$|^constructs.Construct$/), + version: expect.any(String), + }, + }), + }, + }), + }), + }), + }); + + test.done(); + }, + 'token resolution & cfn parameter'(test: Test) { const app = new App(); const stack = new Stack(app, 'mystack'); @@ -137,23 +194,23 @@ nodeunitShim({ test.deepEqual(readJson(assembly.directory, treeArtifact!.file), { version: 'tree-0.1', - tree: { + tree: expect.objectContaining({ id: 'App', path: '', children: { - Tree: { + Tree: expect.objectContaining({ id: 'Tree', path: 'Tree', - }, - mystack: { + }), + mystack: expect.objectContaining({ id: 'mystack', path: 'mystack', children: { - mycfnparam: { + mycfnparam: expect.objectContaining({ id: 'mycfnparam', path: 'mystack/mycfnparam', - }, - mycfnresource: { + }), + mycfnresource: expect.objectContaining({ id: 'mycfnresource', path: 'mystack/mycfnresource', attributes: { @@ -163,11 +220,11 @@ nodeunitShim({ cfnparamkey: { Ref: 'mycfnparam' }, }, }, - }, + }), }, - }, + }), }, - }, + }), }); test.done(); }, @@ -176,7 +233,7 @@ nodeunitShim({ class MyFirstResource extends AbstractCfnResource { public readonly lazykey: string; - constructor(scope: Construct, id: string) { + constructor(scope: CfnConstruct, id: string) { super(scope, id); this.lazykey = Lazy.string({ produce: () => 'LazyResolved!' }); } @@ -191,7 +248,7 @@ nodeunitShim({ class MySecondResource extends AbstractCfnResource { public readonly myprop: string; - constructor(scope: Construct, id: string, myprop: string) { + constructor(scope: CfnConstruct, id: string, myprop: string) { super(scope, id); this.myprop = myprop; } @@ -215,19 +272,19 @@ nodeunitShim({ test.deepEqual(readJson(assembly.directory, treeArtifact!.file), { version: 'tree-0.1', - tree: { + tree: expect.objectContaining({ id: 'App', path: '', children: { - Tree: { + Tree: expect.objectContaining({ id: 'Tree', path: 'Tree', - }, - myfirststack: { + }), + myfirststack: expect.objectContaining({ id: 'myfirststack', path: 'myfirststack', children: { - myfirstresource: { + myfirstresource: expect.objectContaining({ id: 'myfirstresource', path: 'myfirststack/myfirstresource', attributes: { @@ -236,14 +293,14 @@ nodeunitShim({ lazykey: 'LazyResolved!', }, }, - }, + }), }, - }, - mysecondstack: { + }), + mysecondstack: expect.objectContaining({ id: 'mysecondstack', path: 'mysecondstack', children: { - mysecondresource: { + mysecondresource: expect.objectContaining({ id: 'mysecondresource', path: 'mysecondstack/mysecondresource', attributes: { @@ -252,11 +309,11 @@ nodeunitShim({ myprop: 'LazyResolved!', }, }, - }, + }), }, - }, + }), }, - }, + }), }); test.done(); @@ -291,20 +348,20 @@ nodeunitShim({ // assert that the rest of the construct tree is rendered test.deepEqual(readJson(assembly.directory, treeArtifact!.file), { version: 'tree-0.1', - tree: { + tree: expect.objectContaining({ id: 'App', path: '', children: { - Tree: { + Tree: expect.objectContaining({ id: 'Tree', path: 'Tree', - }, - mystack: { + }), + mystack: expect.objectContaining({ id: 'mystack', path: 'mystack', - }, + }), }, - }, + }), }); test.done(); diff --git a/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts b/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts index 0131b7a4acc63..73f8f185f06ba 100644 --- a/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts +++ b/packages/@aws-cdk/core/test/stack-synthesis/new-style-synthesis.test.ts @@ -36,9 +36,9 @@ nodeunitShim({ // THEN -- the S3 url is advertised on the stack artifact const stackArtifact = asm.getStackArtifact('Stack'); - const templateHash = last(stackArtifact.stackTemplateAssetObjectUrl?.split('/')); + const templateObjectKey = last(stackArtifact.stackTemplateAssetObjectUrl?.split('/')); - test.equals(stackArtifact.stackTemplateAssetObjectUrl, `s3://cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}/${templateHash}`); + test.equals(stackArtifact.stackTemplateAssetObjectUrl, `s3://cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}/${templateObjectKey}`); // THEN - the template is in the asset manifest const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; @@ -52,7 +52,7 @@ nodeunitShim({ destinations: { 'current_account-current_region': { bucketName: 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', - objectKey: templateHash, + objectKey: templateObjectKey, assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}', }, }, @@ -111,7 +111,7 @@ nodeunitShim({ // THEN - we have a fixed asset location with region placeholders test.equals(evalCFN(location.bucketName), 'cdk-hnb659fds-assets-the_account-the_region'); - test.equals(evalCFN(location.s3Url), 'https://s3.the_region.domain.aws/cdk-hnb659fds-assets-the_account-the_region/abcdef'); + test.equals(evalCFN(location.s3Url), 'https://s3.the_region.domain.aws/cdk-hnb659fds-assets-the_account-the_region/abcdef.js'); // THEN - object key contains source hash somewhere test.ok(location.objectKey.indexOf('abcdef') > -1); @@ -208,7 +208,7 @@ nodeunitShim({ test.deepEqual(manifest.files?.['file-asset-hash']?.destinations?.['current_account-current_region'], { bucketName: 'file-asset-bucket', - objectKey: 'file-asset-hash', + objectKey: 'file-asset-hash.js', assumeRoleArn: 'file:role:arn', assumeRoleExternalId: 'file-external-id', }); @@ -256,7 +256,7 @@ nodeunitShim({ // THEN test.deepEqual(manifest.files?.['file-asset-hash-with-prefix']?.destinations?.['current_account-current_region'], { bucketName: 'file-asset-bucket', - objectKey: '000000000000/file-asset-hash-with-prefix', + objectKey: '000000000000/file-asset-hash-with-prefix.js', assumeRoleArn: 'file:role:arn', assumeRoleExternalId: 'file-external-id', }); diff --git a/packages/@aws-cdk/core/test/stack.test.ts b/packages/@aws-cdk/core/test/stack.test.ts index 6a54b682108ad..8891dbaa138c6 100644 --- a/packages/@aws-cdk/core/test/stack.test.ts +++ b/packages/@aws-cdk/core/test/stack.test.ts @@ -1,7 +1,10 @@ import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior, testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import { App, CfnCondition, CfnInclude, CfnOutput, CfnParameter, - CfnResource, Construct, Lazy, ScopedAws, Stack, validateString, ISynthesisSession, Tags, LegacyStackSynthesizer, DefaultStackSynthesizer, + CfnResource, Construct, Lazy, ScopedAws, Stack, validateString, + ISynthesisSession, Tags, LegacyStackSynthesizer, DefaultStackSynthesizer, + NestedStack, } from '../lib'; import { Intrinsic } from '../lib/private/intrinsic'; import { resolveReferences } from '../lib/private/refs'; @@ -534,7 +537,69 @@ describe('stack', () => { expect(assembly.getStackArtifact(child1.artifactId).dependencies.map((x: { id: any; }) => x.id)).toEqual([]); expect(assembly.getStackArtifact(child2.artifactId).dependencies.map((x: { id: any; }) => x.id)).toEqual(['ParentChild18FAEF419']); + }); + + test('automatic cross-stack references and manual exports look the same', () => { + // GIVEN: automatic + const appA = new App(); + const producerA = new Stack(appA, 'Producer'); + const consumerA = new Stack(appA, 'Consumer'); + const resourceA = new CfnResource(producerA, 'Resource', { type: 'AWS::Resource' }); + new CfnOutput(consumerA, 'SomeOutput', { value: `${resourceA.getAtt('Att')}` }); + + // GIVEN: manual + const appM = new App(); + const producerM = new Stack(appM, 'Producer'); + const resourceM = new CfnResource(producerM, 'Resource', { type: 'AWS::Resource' }); + producerM.exportValue(resourceM.getAtt('Att')); + + // THEN - producers are the same + const templateA = appA.synth().getStackByName(producerA.stackName).template; + const templateM = appM.synth().getStackByName(producerM.stackName).template; + + expect(templateA).toEqual(templateM); + }); + + test('automatic cross-stack references and manual exports look the same: nested stack edition', () => { + // GIVEN: automatic + const appA = new App(); + const producerA = new Stack(appA, 'Producer'); + const nestedA = new NestedStack(producerA, 'Nestor'); + const resourceA = new CfnResource(nestedA, 'Resource', { type: 'AWS::Resource' }); + + const consumerA = new Stack(appA, 'Consumer'); + new CfnOutput(consumerA, 'SomeOutput', { value: `${resourceA.getAtt('Att')}` }); + + // GIVEN: manual + const appM = new App(); + const producerM = new Stack(appM, 'Producer'); + const nestedM = new NestedStack(producerM, 'Nestor'); + const resourceM = new CfnResource(nestedM, 'Resource', { type: 'AWS::Resource' }); + producerM.exportValue(resourceM.getAtt('Att')); + + // THEN - producers are the same + const templateA = appA.synth().getStackByName(producerA.stackName).template; + const templateM = appM.synth().getStackByName(producerM.stackName).template; + + expect(templateA).toEqual(templateM); + }); + + test('manual exports require a name if not supplying a resource attribute', () => { + const app = new App(); + const stack = new Stack(app, 'Stack'); + + expect(() => { + stack.exportValue('someValue'); + }).toThrow(/or make sure to export a resource attribute/); + }); + + test('manual exports can also just be used to create an export of anything', () => { + const app = new App(); + const stack = new Stack(app, 'Stack'); + const importV = stack.exportValue('someValue', { name: 'MyExport' }); + + expect(stack.resolve(importV)).toEqual({ 'Fn::ImportValue': 'MyExport' }); }); test('CfnSynthesisError is ignored when preparing cross references', () => { @@ -880,46 +945,22 @@ describe('stack', () => { }); - test('stack.templateFile is the name of the template file emitted to the cloud assembly (default is to use the stack name)', () => { - // GIVEN - const app = new App(); - - // WHEN - const stack1 = new Stack(app, 'MyStack1'); - const stack2 = new Stack(app, 'MyStack2', { stackName: 'MyRealStack2' }); - - // THEN - expect(stack1.templateFile).toEqual('MyStack1.template.json'); - expect(stack2.templateFile).toEqual('MyRealStack2.template.json'); - - }); - - test('when feature flag is enabled we will use the artifact id as the template name', () => { - // GIVEN - const app = new App({ - context: { - [cxapi.ENABLE_STACK_NAME_DUPLICATES_CONTEXT]: 'true', - }, - }); - - // WHEN - const stack1 = new Stack(app, 'MyStack1'); - const stack2 = new Stack(app, 'MyStack2', { stackName: 'MyRealStack2' }); - - // THEN - expect(stack1.templateFile).toEqual('MyStack1.template.json'); - expect(stack2.templateFile).toEqual('MyStack2.template.json'); - - }); - describe('@aws-cdk/core:enableStackNameDuplicates', () => { describe('disabled (default)', () => { - test('artifactId and templateFile use the stack name', () => { - // GIVEN - const app = new App(); + testLegacyBehavior('stack.templateFile is the name of the template file emitted to the cloud assembly (default is to use the stack name)', App, (app) => { + // WHEN + const stack1 = new Stack(app, 'MyStack1'); + const stack2 = new Stack(app, 'MyStack2', { stackName: 'MyRealStack2' }); + + // THEN + expect(stack1.templateFile).toEqual('MyStack1.template.json'); + expect(stack2.templateFile).toEqual('MyRealStack2.template.json'); + + }); + testLegacyBehavior('artifactId and templateFile use the stack name', App, (app) => { // WHEN const stack1 = new Stack(app, 'MyStack1', { stackName: 'thestack' }); const assembly = app.synth(); @@ -928,15 +969,12 @@ describe('stack', () => { expect(stack1.artifactId).toEqual('thestack'); expect(stack1.templateFile).toEqual('thestack.template.json'); expect(assembly.getStackArtifact(stack1.artifactId).templateFile).toEqual('thestack.template.json'); - }); }); describe('enabled', () => { - test('allows using the same stack name for two stacks (i.e. in different regions)', () => { - // GIVEN - const app = new App({ context: { [cxapi.ENABLE_STACK_NAME_DUPLICATES_CONTEXT]: 'true' } }); - + const flags = { [cxapi.ENABLE_STACK_NAME_DUPLICATES_CONTEXT]: 'true' }; + testFutureBehavior('allows using the same stack name for two stacks (i.e. in different regions)', flags, App, (app) => { // WHEN const stack1 = new Stack(app, 'MyStack1', { stackName: 'thestack' }); const stack2 = new Stack(app, 'MyStack2', { stackName: 'thestack' }); @@ -947,13 +985,9 @@ describe('stack', () => { expect(assembly.getStackArtifact(stack2.artifactId).templateFile).toEqual('MyStack2.template.json'); expect(stack1.templateFile).toEqual('MyStack1.template.json'); expect(stack2.templateFile).toEqual('MyStack2.template.json'); - }); - test('artifactId and templateFile use the unique id and not the stack name', () => { - // GIVEN - const app = new App({ context: { [cxapi.ENABLE_STACK_NAME_DUPLICATES_CONTEXT]: 'true' } }); - + testFutureBehavior('artifactId and templateFile use the unique id and not the stack name', flags, App, (app) => { // WHEN const stack1 = new Stack(app, 'MyStack1', { stackName: 'thestack' }); const assembly = app.synth(); @@ -962,7 +996,16 @@ describe('stack', () => { expect(stack1.artifactId).toEqual('MyStack1'); expect(stack1.templateFile).toEqual('MyStack1.template.json'); expect(assembly.getStackArtifact(stack1.artifactId).templateFile).toEqual('MyStack1.template.json'); + }); + + testFutureBehavior('when feature flag is enabled we will use the artifact id as the template name', flags, App, (app) => { + // WHEN + const stack1 = new Stack(app, 'MyStack1'); + const stack2 = new Stack(app, 'MyStack2', { stackName: 'MyRealStack2' }); + // THEN + expect(stack1.templateFile).toEqual('MyStack1.template.json'); + expect(stack2.templateFile).toEqual('MyStack2.template.json'); }); }); @@ -1120,4 +1163,4 @@ class StackWithPostProcessor extends Stack { return template; } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/test/synthesis.test.ts b/packages/@aws-cdk/core/test/synthesis.test.ts index 464e544cc562a..8b5200c1dfb72 100644 --- a/packages/@aws-cdk/core/test/synthesis.test.ts +++ b/packages/@aws-cdk/core/test/synthesis.test.ts @@ -28,13 +28,13 @@ nodeunitShim({ }); test.deepEqual(readJson(session.directory, 'tree.json'), { version: 'tree-0.1', - tree: { + tree: expect.objectContaining({ id: 'App', path: '', children: { - Tree: { id: 'Tree', path: 'Tree' }, + Tree: expect.objectContaining({ id: 'Tree', path: 'Tree' }), }, - }, + }), }); test.done(); }, diff --git a/packages/@aws-cdk/core/test/tag-aspect.test.ts b/packages/@aws-cdk/core/test/tag-aspect.test.ts index cb2c5363e2153..b0871e2d13c02 100644 --- a/packages/@aws-cdk/core/test/tag-aspect.test.ts +++ b/packages/@aws-cdk/core/test/tag-aspect.test.ts @@ -6,7 +6,7 @@ class TaggableResource extends CfnResource { public readonly tags: TagManager; constructor(scope: Construct, id: string, props: CfnResourceProps) { super(scope, id, props); - const tags = props.properties === undefined ? undefined : props.properties.tags; + const tags = props.properties?.tags; this.tags = new TagManager(TagType.STANDARD, 'AWS::Fake::Resource', tags); } public testProperties() { @@ -18,7 +18,7 @@ class AsgTaggableResource extends CfnResource { public readonly tags: TagManager; constructor(scope: Construct, id: string, props: CfnResourceProps) { super(scope, id, props); - const tags = props.properties === undefined ? undefined : props.properties.tags; + const tags = props.properties?.tags; this.tags = new TagManager(TagType.AUTOSCALING_GROUP, 'AWS::Fake::Resource', tags); } public testProperties() { @@ -30,7 +30,7 @@ class MapTaggableResource extends CfnResource { public readonly tags: TagManager; constructor(scope: Construct, id: string, props: CfnResourceProps) { super(scope, id, props); - const tags = props.properties === undefined ? undefined : props.properties.tags; + const tags = props.properties?.tags; this.tags = new TagManager(TagType.MAP, 'AWS::Fake::Resource', tags); } public testProperties() { diff --git a/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts b/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts index 3ae10fdf2e560..9da696f268b92 100644 --- a/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts +++ b/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts @@ -3,12 +3,16 @@ import * as cfn from '@aws-cdk/aws-cloudformation'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; -import { Construct as CoreConstruct, Duration } from '@aws-cdk/core'; +import { Duration } from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as consts from './runtime/consts'; import { calculateRetryPolicy } from './util'; import { WaiterStateMachine } from './waiter-state-machine'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + const RUNTIME_HANDLER_PATH = path.join(__dirname, 'runtime'); const FRAMEWORK_HANDLER_TIMEOUT = Duration.minutes(15); // keep it simple for now diff --git a/packages/@aws-cdk/custom-resources/lib/provider-framework/waiter-state-machine.ts b/packages/@aws-cdk/custom-resources/lib/provider-framework/waiter-state-machine.ts index 513e18d7f9c8b..6799fb3178123 100644 --- a/packages/@aws-cdk/custom-resources/lib/provider-framework/waiter-state-machine.ts +++ b/packages/@aws-cdk/custom-resources/lib/provider-framework/waiter-state-machine.ts @@ -1,6 +1,10 @@ import { Grant, IGrantable, PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import { IFunction } from '@aws-cdk/aws-lambda'; -import { CfnResource, Construct, Duration, Stack } from '@aws-cdk/core'; +import { CfnResource, Duration, Stack } from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; export interface WaiterStateMachineProps { /** diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index cab6d86231e9a..543024ca21a01 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -75,18 +75,18 @@ "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", - "@types/aws-lambda": "^8.10.64", + "@types/aws-lambda": "^8.10.71", "@types/fs-extra": "^8.1.1", - "@types/sinon": "^9.0.9", - "aws-sdk": "^2.830.0", + "@types/sinon": "^9.0.10", + "aws-sdk": "^2.842.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "fs-extra": "^9.1.0", - "nock": "^13.0.5", + "nock": "^13.0.7", "pkglint": "0.0.0", - "sinon": "^9.2.1" + "sinon": "^9.2.4" }, "dependencies": { "@aws-cdk/aws-cloudformation": "0.0.0", diff --git a/packages/@aws-cdk/cx-api/NOTICE b/packages/@aws-cdk/cx-api/NOTICE index 070bcdddebcc3..f2197d8374ddd 100644 --- a/packages/@aws-cdk/cx-api/NOTICE +++ b/packages/@aws-cdk/cx-api/NOTICE @@ -20,4 +20,36 @@ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ----------------- \ No newline at end of file +---------------- + +** lru-cache - https://www.npmjs.com/package/lru-cache +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---------------- + +** yallist - https://www.npmjs.com/package/yallist +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/packages/@aws-cdk/cx-api/lib/features.ts b/packages/@aws-cdk/cx-api/lib/features.ts index c91607b587eb0..57c9a5738ee96 100644 --- a/packages/@aws-cdk/cx-api/lib/features.ts +++ b/packages/@aws-cdk/cx-api/lib/features.ts @@ -83,7 +83,7 @@ export const KMS_DEFAULT_KEY_POLICIES = '@aws-cdk/aws-kms:defaultKeyPolicies'; /** * Change the old 's3:PutObject*' permission to 's3:PutObject' on Bucket, * as the former includes 's3:PutObjectAcl', - * which allows changing the visibility of an object written to the Bucket. + * which could be used to grant read/write object access to IAM principals in other accounts. * Use a feature flag to make sure existing customers who might be relying * on the overly-broad permissions are not broken. */ @@ -115,6 +115,13 @@ export const FUTURE_FLAGS = { // [NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: 'true', }; +/** + * The list of future flags that are now expired. This is going to be used to identify + * and block usages of old feature flags in the new major version of CDK. + */ +export const FUTURE_FLAGS_EXPIRED: string[] = [ +]; + /** * The set of defaults that should be applied if the feature flag is not * explicitly configured. diff --git a/packages/@aws-cdk/cx-api/package.json b/packages/@aws-cdk/cx-api/package.json index 62e692fd08012..dd015ee63986c 100644 --- a/packages/@aws-cdk/cx-api/package.json +++ b/packages/@aws-cdk/cx-api/package.json @@ -57,14 +57,14 @@ }, "dependencies": { "@aws-cdk/cloud-assembly-schema": "0.0.0", - "semver": "^7.3.2" + "semver": "^7.3.4" }, "peerDependencies": { "@aws-cdk/cloud-assembly-schema": "0.0.0" }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "@types/mock-fs": "^4.13.0", "@types/semver": "^7.3.4", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/pipelines/lib/actions/deploy-cdk-stack-action.ts b/packages/@aws-cdk/pipelines/lib/actions/deploy-cdk-stack-action.ts index 9fe520112931f..592b5b93e3855 100644 --- a/packages/@aws-cdk/pipelines/lib/actions/deploy-cdk-stack-action.ts +++ b/packages/@aws-cdk/pipelines/lib/actions/deploy-cdk-stack-action.ts @@ -1,11 +1,10 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as cfn from '@aws-cdk/aws-cloudformation'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as cpactions from '@aws-cdk/aws-codepipeline-actions'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; -import { Aws, Stack } from '@aws-cdk/core'; +import { Aws, CfnCapabilities, Stack } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct, Node } from 'constructs'; import { appOf, assemblyBuilderOf } from '../private/construct-internals'; @@ -249,7 +248,7 @@ export class DeployCdkStackAction implements codepipeline.IAction { role: props.actionRole, deploymentRole: props.cloudFormationExecutionRole, region: props.region, - capabilities: [cfn.CloudFormationCapabilities.NAMED_IAM, cfn.CloudFormationCapabilities.AUTO_EXPAND], + cfnCapabilities: [CfnCapabilities.NAMED_IAM, CfnCapabilities.AUTO_EXPAND], templateConfiguration: props.templateConfigurationPath ? props.cloudAssemblyInput.atPath(props.templateConfigurationPath) : undefined, }); this.executeChangeSetAction = new cpactions.CloudFormationExecuteChangeSetAction({ @@ -378,4 +377,4 @@ interface TemplateConfiguration { */ function writeTemplateConfiguration(filename: string, config: TemplateConfiguration) { fs.writeFileSync(filename, JSON.stringify(config, undefined, 2), { encoding: 'utf-8' }); -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts b/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts index 0c661b61e9251..10b1bccbab965 100644 --- a/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts +++ b/packages/@aws-cdk/pipelines/lib/actions/publish-assets-action.ts @@ -10,6 +10,7 @@ import { Construct } from 'constructs'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line import { Construct as CoreConstruct } from '@aws-cdk/core'; +import { toPosixPath } from '../private/fs'; /** * Type of the asset that is being published @@ -150,7 +151,7 @@ export class PublishAssetsAction extends CoreConstruct implements codepipeline.I * Manifest path should be relative to the root Cloud Assembly. */ public addPublishCommand(relativeManifestPath: string, assetSelector: string) { - const command = `cdk-assets --path "${relativeManifestPath}" --verbose publish "${assetSelector}"`; + const command = `cdk-assets --path "${toPosixPath(relativeManifestPath)}" --verbose publish "${assetSelector}"`; if (!this.commands.includes(command)) { this.commands.push(command); } diff --git a/packages/@aws-cdk/pipelines/lib/pipeline.ts b/packages/@aws-cdk/pipelines/lib/pipeline.ts index 6e39f92b7582d..20f751078646e 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 } from '@aws-cdk/core'; +import { Annotations, App, Aws, 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'; @@ -516,6 +516,40 @@ class AssetPublishing extends CoreConstruct { // Artifact access this.pipeline.artifactBucket.grantRead(assetRole); + // VPC permissions required for CodeBuild + // Normally CodeBuild itself takes care of this but we're creating a singleton role so now + // we need to do this. + if (this.props.vpc) { + assetRole.attachInlinePolicy(new iam.Policy(assetRole, 'VpcPolicy', { + statements: [ + new iam.PolicyStatement({ + resources: [`arn:${Aws.PARTITION}:ec2:${Aws.REGION}:${Aws.ACCOUNT_ID}:network-interface/*`], + actions: ['ec2:CreateNetworkInterfacePermission'], + conditions: { + StringEquals: { + 'ec2:Subnet': this.props.vpc + .selectSubnets(this.props.subnetSelection).subnetIds + .map(si => `arn:${Aws.PARTITION}:ec2:${Aws.REGION}:${Aws.ACCOUNT_ID}:subnet/${si}`), + 'ec2:AuthorizedService': 'codebuild.amazonaws.com', + }, + }, + }), + new iam.PolicyStatement({ + resources: ['*'], + actions: [ + 'ec2:CreateNetworkInterface', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DeleteNetworkInterface', + 'ec2:DescribeSubnets', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeDhcpOptions', + 'ec2:DescribeVpcs', + ], + }), + ], + })); + } + this.assetRoles[assetType] = assetRole.withoutPolicyUpdates(); return this.assetRoles[assetType]; } diff --git a/packages/@aws-cdk/pipelines/lib/synths/simple-synth-action.ts b/packages/@aws-cdk/pipelines/lib/synths/simple-synth-action.ts index d04408b9175cd..415f49f703e26 100644 --- a/packages/@aws-cdk/pipelines/lib/synths/simple-synth-action.ts +++ b/packages/@aws-cdk/pipelines/lib/synths/simple-synth-action.ts @@ -6,11 +6,15 @@ import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; -import { Construct, Stack } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { cloudAssemblyBuildSpecDir } from '../private/construct-internals'; import { toPosixPath } from '../private/fs'; import { copyEnvironmentVariables, filterEmpty } from './_util'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Configuration options for a SimpleSynth */ @@ -320,7 +324,6 @@ export class SimpleSynthAction implements codepipeline.IAction, iam.IGrantable { const environmentVariables = { ...copyEnvironmentVariables(...this.props.copyEnvironmentVariables || []), - ...this.props.environmentVariables, }; // A hash over the values that make the CodeBuild Project unique (and necessary @@ -360,6 +363,7 @@ export class SimpleSynthAction implements codepipeline.IAction, iam.IGrantable { // Hence, the pipeline will be restarted. This is necessary if the users // adds (for example) build or test commands to the buildspec. environmentVariables: { + ...this.props.environmentVariables, _PROJECT_CONFIG_HASH: { value: projectConfigHash }, }, project, diff --git a/packages/@aws-cdk/pipelines/lib/validation/shell-script-action.ts b/packages/@aws-cdk/pipelines/lib/validation/shell-script-action.ts index 1b439fe309c7e..59f64555a76b5 100644 --- a/packages/@aws-cdk/pipelines/lib/validation/shell-script-action.ts +++ b/packages/@aws-cdk/pipelines/lib/validation/shell-script-action.ts @@ -4,9 +4,13 @@ import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; -import { Construct } from '@aws-cdk/core'; + import { StackOutput } from '../stage'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + /** * Properties for ShellScriptAction */ @@ -61,6 +65,13 @@ export interface ShellScriptActionProps { */ readonly environment?: codebuild.BuildEnvironment + /** + * Environment variables to send into build + * + * @default - No additional environment variables + */ + readonly environmentVariables?: Record; + /** * RunOrder for this action * @@ -210,6 +221,7 @@ export class ShellScriptAction implements codepipeline.IAction, iam.IGrantable { extraInputs: inputs.slice(1), runOrder: this.props.runOrder ?? 100, project: this._project, + environmentVariables: this.props.environmentVariables, }); // Replace the placeholder actionProperties at the last minute this._actionProperties = this._action.actionProperties; diff --git a/packages/@aws-cdk/pipelines/package.json b/packages/@aws-cdk/pipelines/package.json index 6df06d00c1846..b13b5877cace7 100644 --- a/packages/@aws-cdk/pipelines/package.json +++ b/packages/@aws-cdk/pipelines/package.json @@ -50,8 +50,7 @@ "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", - "@aws-cdk/cx-api": "0.0.0", - "@aws-cdk/aws-cloudformation": "0.0.0" + "@aws-cdk/cx-api": "0.0.0" }, "dependencies": { "constructs": "^3.2.0", @@ -64,8 +63,7 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", - "@aws-cdk/cx-api": "0.0.0", - "@aws-cdk/aws-cloudformation": "0.0.0" + "@aws-cdk/cx-api": "0.0.0" }, "bundledDependencies": [], "keywords": [ diff --git a/packages/@aws-cdk/pipelines/test/builds.test.ts b/packages/@aws-cdk/pipelines/test/builds.test.ts index 57521eb37c01b..f0b18e1e36442 100644 --- a/packages/@aws-cdk/pipelines/test/builds.test.ts +++ b/packages/@aws-cdk/pipelines/test/builds.test.ts @@ -149,6 +149,49 @@ test.each([['npm'], ['yarn']])('%s assumes no build step by default', (npmYarn) }); }); +test('environmentVariables must be rendered in the action', () => { + // WHEN + new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { + sourceArtifact, + cloudAssemblyArtifact, + synthAction: new cdkp.SimpleSynthAction({ + sourceArtifact, + cloudAssemblyArtifact, + environmentVariables: { + VERSION: { value: codepipeline.GlobalVariables.executionId }, + }, + synthCommand: 'synth', + }), + }); + + // THEN + const theHash = Capture.aString(); + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Build', + Actions: [ + objectLike({ + Name: 'Synth', + Configuration: objectLike({ + EnvironmentVariables: encodedJson([ + { + name: 'VERSION', + type: 'PLAINTEXT', + value: '#{codepipeline.PipelineExecutionId}', + }, + { + name: '_PROJECT_CONFIG_HASH', + type: 'PLAINTEXT', + value: theHash.capture(), + }, + ]), + }), + }), + ], + }), + }); +}); + test('complex setup with environemnt variables still renders correct project', () => { // WHEN new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { @@ -184,11 +227,6 @@ test('complex setup with environemnt variables still renders correct project', ( Type: 'PLAINTEXT', Value: 'InnerValue', }, - { - Name: 'SOME_ENV_VAR', - Type: 'PLAINTEXT', - Value: 'SomeValue', - }, ], }), Source: { @@ -293,27 +331,27 @@ test('Standard (NPM) synth can run in a VPC', () => { expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { VpcConfig: { SecurityGroupIds: [ - { - 'Fn::GetAtt': [ - 'CdkPipelineBuildSynthCdkBuildProjectSecurityGroupEA44D7C2', - 'GroupId', - ], - }, + { 'Fn::GetAtt': ['CdkPipelineBuildSynthCdkBuildProjectSecurityGroupEA44D7C2', 'GroupId'] }, ], Subnets: [ - { - Ref: 'NpmSynthTestVpcPrivateSubnet1Subnet81E3AA56', - }, - { - Ref: 'NpmSynthTestVpcPrivateSubnet2SubnetC1CA3EF0', - }, - { - Ref: 'NpmSynthTestVpcPrivateSubnet3SubnetA04163EE', - }, + { Ref: 'NpmSynthTestVpcPrivateSubnet1Subnet81E3AA56' }, + { Ref: 'NpmSynthTestVpcPrivateSubnet2SubnetC1CA3EF0' }, + { Ref: 'NpmSynthTestVpcPrivateSubnet3SubnetA04163EE' }, ], - VpcId: { - Ref: 'NpmSynthTestVpc5E703F25', - }, + VpcId: { Ref: 'NpmSynthTestVpc5E703F25' }, + }, + }); + + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Roles: [ + { Ref: 'CdkPipelineBuildSynthCdkBuildProjectRole5E173C62' }, + ], + PolicyDocument: { + Statement: arrayWith({ + Action: arrayWith('ec2:DescribeSecurityGroups'), + Effect: 'Allow', + Resource: '*', + }), }, }); }); @@ -386,8 +424,10 @@ test('Pipeline action contains a hash that changes as the buildspec changes', () const hash4 = synthWithAction((sa, cxa) => cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact: sa, cloudAssemblyArtifact: cxa, - environmentVariables: { - xyz: { value: 'SOME-VALUE' }, + environment: { + environmentVariables: { + xyz: { value: 'SOME-VALUE' }, + }, }, })); @@ -480,6 +520,9 @@ test('SimpleSynthAction can reference an imported ECR repo', () => { }, }), }); + + // THEN -- no exception (necessary for linter) + expect(true).toBeTruthy(); }); function npmYarnBuild(npmYarn: string) { diff --git a/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts b/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts index c10906e7ad7bb..01b1d815bea17 100644 --- a/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts +++ b/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts @@ -2,6 +2,7 @@ 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 ec2 from '@aws-cdk/aws-ec2'; 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'; @@ -15,366 +16,393 @@ let app: TestApp; let pipelineStack: Stack; let pipeline: cdkp.CdkPipeline; -beforeEach(() => { - app = new TestApp(); - pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); - pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk'); -}); - -afterEach(() => { - app.cleanup(); -}); -test('no assets stage if the application has no assets', () => { - // WHEN - pipeline.addApplicationStage(new PlainStackApp(app, 'App')); +describe('basic pipeline', () => { + beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); + pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk'); + }); - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: notMatching(arrayWith(objectLike({ - Name: 'Assets', - }))), + afterEach(() => { + app.cleanup(); }); -}); -describe('asset stage placement', () => { - test('assets stage comes before any user-defined stages', () => { + test('no assets stage if the application has no assets', () => { // WHEN - pipeline.addApplicationStage(new FileAssetApp(app, 'App')); + pipeline.addApplicationStage(new PlainStackApp(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' }), - ], + Stages: notMatching(arrayWith(objectLike({ + Name: 'Assets', + }))), }); }); - 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 })], - }, - ], + 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' }), + ], + }); }); - pipeline = new cdkp.CdkPipeline(pipelineStack, 'CdkEmptyPipeline', { - cloudAssemblyArtifact: cloudAssemblyArtifact, - selfMutating: false, - codePipeline: existingCodePipeline, - // No source/build actions + + 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' }), + ], + }); }); - 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('up to 50 assets fit in a single stage', () => { + test('command line properly locates assets in subassembly', () => { // WHEN - pipeline.addApplicationStage(new MegaAssetsApp(app, 'App', { numAssets: 50 })); + pipeline.addApplicationStage(new FileAssetApp(app, 'FileAssetApp')); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: [ - objectLike({ Name: 'Source' }), - objectLike({ Name: 'Build' }), - objectLike({ Name: 'UpdatePipeline' }), - objectLike({ Name: 'Assets' }), - objectLike({ Name: 'App' }), - ], + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:4.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: arrayWith(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH}:current_account-current_region"`), + }, + }, + })), + }, }); }); - test('51 assets triggers a second stage', () => { + test('multiple assets are published in parallel', () => { // WHEN - pipeline.addApplicationStage(new MegaAssetsApp(app, 'App', { numAssets: 51 })); + pipeline.addApplicationStage(new TwoFileAssetsApp(app, 'FileAssetApp')); // 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' }), - ], + Stages: arrayWith({ + Name: 'Assets', + Actions: [ + objectLike({ RunOrder: 1 }), + objectLike({ RunOrder: 1 }), + ], + }), }); }); - test('101 assets triggers a third stage', () => { + test('assets are also published when using the lower-level addStackArtifactDeployment', () => { + // GIVEN + const asm = new FileAssetApp(app, 'FileAssetApp').synth(); + // WHEN - pipeline.addApplicationStage(new MegaAssetsApp(app, 'App', { numAssets: 101 })); + pipeline.addStage('SomeStage').addStackArtifactDeployment(asm.getStackByName('FileAssetApp-Stack')); // 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' }), - ], + Stages: arrayWith({ + Name: 'Assets', + Actions: [ + objectLike({ + Name: 'FileAsset1', + RunOrder: 1, + }), + ], + }), }); }); -}); -test('command line properly locates assets in subassembly', () => { - // WHEN - pipeline.addApplicationStage(new FileAssetApp(app, 'FileAssetApp')); + test('file image asset publishers do not use privilegedmode, have right AssumeRole', () => { + // WHEN + pipeline.addApplicationStage(new FileAssetApp(app, 'FileAssetApp')); - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:4.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: arrayWith(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH}:current_account-current_region"`), + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: arrayWith(stringLike('cdk-assets *')), + }, }, - }, - })), - }, - }); -}); - -test('multiple assets are published in parallel', () => { - // WHEN - pipeline.addApplicationStage(new TwoFileAssetsApp(app, 'FileAssetApp')); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Assets', - Actions: [ - objectLike({ RunOrder: 1 }), - objectLike({ RunOrder: 1 }), - ], - }), - }); -}); + })), + }, + Environment: objectLike({ + PrivilegedMode: false, + Image: 'aws/codebuild/standard:4.0', + }), + }); -test('assets are also published when using the lower-level addStackArtifactDeployment', () => { - // GIVEN - const asm = new FileAssetApp(app, 'FileAssetApp').synth(); - - // WHEN - pipeline.addStage('SomeStage').addStackArtifactDeployment(asm.getStackByName('FileAssetApp-Stack')); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ - Name: 'Assets', - Actions: [ - objectLike({ - Name: 'FileAsset1', - RunOrder: 1, + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith({ + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: 'arn:*:iam::*:role/*-file-publishing-role-*', }), - ], - }), + }, + }); }); -}); -test('file image asset publishers do not use privilegedmode, have right AssumeRole', () => { - // WHEN - pipeline.addApplicationStage(new FileAssetApp(app, 'FileAssetApp')); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: arrayWith(stringLike('cdk-assets *')), - }, - }, - })), - }, - Environment: objectLike({ - PrivilegedMode: false, - Image: 'aws/codebuild/standard:4.0', - }), - }); + test('docker image asset publishers use privilegedmode, have right AssumeRole', () => { + // WHEN + pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith({ - Action: 'sts:AssumeRole', - Effect: 'Allow', - Resource: 'arn:*:iam::*:role/*-file-publishing-role-*', + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + build: { + commands: arrayWith(stringLike('cdk-assets *')), + }, + }, + })), + }, + Environment: objectLike({ + Image: 'aws/codebuild/standard:4.0', + PrivilegedMode: true, }), - }, + }); + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith({ + Action: 'sts:AssumeRole', + Effect: 'Allow', + Resource: 'arn:*:iam::*:role/*-image-publishing-role-*', + }), + }, + }); }); -}); -test('docker image asset publishers use privilegedmode, have right AssumeRole', () => { - // WHEN - pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - build: { - commands: arrayWith(stringLike('cdk-assets *')), + test('can control fix/CLI version used in pipeline selfupdate', () => { + // WHEN + const stack2 = new Stack(app, 'Stack2', { env: PIPELINE_ENV }); + const pipeline2 = new TestGitHubNpmPipeline(stack2, 'Cdk2', { + cdkCliVersion: '1.2.3', + }); + pipeline2.addApplicationStage(new FileAssetApp(stack2, 'FileAssetApp')); + + // THEN + expect(stack2).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:4.0', + }, + Source: { + BuildSpec: encodedJson(deepObjectLike({ + phases: { + install: { + commands: 'npm install -g cdk-assets@1.2.3', + }, }, - }, - })), - }, - Environment: objectLike({ - Image: 'aws/codebuild/standard:4.0', - PrivilegedMode: true, - }), - }); - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { - PolicyDocument: { - Statement: arrayWith({ - Action: 'sts:AssumeRole', - Effect: 'Allow', - Resource: 'arn:*:iam::*:role/*-image-publishing-role-*', - }), - }, + })), + }, + }); }); -}); -test('docker image asset can use a VPC', () => { - // WHEN - pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); - - // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - VpcConfig: objectLike({ - SecurityGroupIds: [ - { - 'Fn::GetAtt': [ - 'CdkAssetsDockerAsset1SecurityGroup078F5C66', - 'GroupId', - ], - }, - ], - Subnets: [ - { - Ref: 'TestVpcPrivateSubnet1SubnetCC65D771', - }, - { - Ref: 'TestVpcPrivateSubnet2SubnetDE0C64A2', + describe('asset roles and policies', () => { + test('includes file publishing assets role for apps with file assets', () => { + pipeline.addApplicationStage(new FileAssetApp(app, 'App1')); + + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codebuild.amazonaws.com', + AWS: { + 'Fn::Join': ['', [ + 'arn:', { Ref: 'AWS::Partition' }, `:iam::${PIPELINE_ENV.account}:root`, + ]], + }, + }, + }], }, - { - Ref: 'TestVpcPrivateSubnet3Subnet2311D32F', + }); + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', + expectedAssetRolePolicy('arn:*:iam::*:role/*-file-publishing-role-*', 'CdkAssetsFileRole6BE17A07')); + }); + + test('includes image publishing assets role for apps with Docker assets', () => { + pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); + + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [{ + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'codebuild.amazonaws.com', + AWS: { + 'Fn::Join': ['', [ + 'arn:', { Ref: 'AWS::Partition' }, `:iam::${PIPELINE_ENV.account}:root`, + ]], + }, + }, + }], }, - ], - VpcId: { - Ref: 'TestVpcE77CE678', - }, - }), + }); + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', + expectedAssetRolePolicy('arn:*:iam::*:role/*-image-publishing-role-*', 'CdkAssetsDockerRole484B6DD3')); + }); + + test('includes both roles for apps with both file and Docker assets', () => { + pipeline.addApplicationStage(new FileAssetApp(app, 'App1')); + pipeline.addApplicationStage(new DockerAssetApp(app, 'App2')); + + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', + expectedAssetRolePolicy('arn:*:iam::*:role/*-file-publishing-role-*', 'CdkAssetsFileRole6BE17A07')); + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', + expectedAssetRolePolicy('arn:*:iam::*:role/*-image-publishing-role-*', 'CdkAssetsDockerRole484B6DD3')); + }); }); }); -test('can control fix/CLI version used in pipeline selfupdate', () => { - // WHEN - const stack2 = new Stack(app, 'Stack2', { env: PIPELINE_ENV }); - const pipeline2 = new TestGitHubNpmPipeline(stack2, 'Cdk2', { - cdkCliVersion: '1.2.3', +describe('pipeline with VPC', () => { + let vpc: ec2.Vpc; + beforeEach(() => { + app = new TestApp(); + pipelineStack = new Stack(app, 'PipelineStack', { env: PIPELINE_ENV }); + vpc = new ec2.Vpc(pipelineStack, 'Vpc'); + pipeline = new TestGitHubNpmPipeline(pipelineStack, 'Cdk', { + vpc, + }); }); - pipeline2.addApplicationStage(new FileAssetApp(stack2, 'FileAssetApp')); - // THEN - expect(stack2).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: { - Image: 'aws/codebuild/standard:4.0', - }, - Source: { - BuildSpec: encodedJson(deepObjectLike({ - phases: { - install: { - commands: 'npm install -g cdk-assets@1.2.3', - }, - }, - })), - }, + afterEach(() => { + app.cleanup(); }); -}); -describe('asset roles and policies', () => { - test('includes file publishing assets role for apps with file assets', () => { - pipeline.addApplicationStage(new FileAssetApp(app, 'App1')); + test('asset CodeBuild Project uses VPC subnets', () => { + // WHEN + pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Role', { - AssumeRolePolicyDocument: { - Statement: [{ - Action: 'sts:AssumeRole', - Effect: 'Allow', - Principal: { - Service: 'codebuild.amazonaws.com', - AWS: { - 'Fn::Join': ['', [ - 'arn:', { Ref: 'AWS::Partition' }, `:iam::${PIPELINE_ENV.account}:root`, - ]], - }, - }, - }], - }, + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + VpcConfig: objectLike({ + SecurityGroupIds: [ + { 'Fn::GetAtt': ['CdkAssetsDockerAsset1SecurityGroup078F5C66', 'GroupId'] }, + ], + Subnets: [ + { Ref: 'VpcPrivateSubnet1Subnet536B997A' }, + { Ref: 'VpcPrivateSubnet2Subnet3788AAA1' }, + { Ref: 'VpcPrivateSubnet3SubnetF258B56E' }, + ], + VpcId: { Ref: 'Vpc8378EB38' }, + }), }); - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', - expectedAssetRolePolicy('arn:*:iam::*:role/*-file-publishing-role-*', 'CdkAssetsFileRole6BE17A07')); }); - test('includes image publishing assets role for apps with Docker assets', () => { - pipeline.addApplicationStage(new DockerAssetApp(app, 'App1')); + test('Pipeline-generated CodeBuild Projects have appropriate execution role permissions', () => { + // WHEN + pipeline.addApplicationStage(new DockerAssetApp(app, 'DockerAssetApp')); - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Role', { - AssumeRolePolicyDocument: { - Statement: [{ - Action: 'sts:AssumeRole', + // THEN + + // Assets Project + expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Roles: [ + { Ref: 'CdkAssetsDockerRole484B6DD3' }, + ], + PolicyDocument: { + Statement: arrayWith({ + Action: arrayWith('ec2:DescribeSecurityGroups'), Effect: 'Allow', - Principal: { - Service: 'codebuild.amazonaws.com', - AWS: { - 'Fn::Join': ['', [ - 'arn:', { Ref: 'AWS::Partition' }, `:iam::${PIPELINE_ENV.account}:root`, - ]], - }, - }, - }], + Resource: '*', + }), }, }); - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', - expectedAssetRolePolicy('arn:*:iam::*:role/*-image-publishing-role-*', 'CdkAssetsDockerRole484B6DD3')); - }); - - test('includes both roles for apps with both file and Docker assets', () => { - pipeline.addApplicationStage(new FileAssetApp(app, 'App1')); - pipeline.addApplicationStage(new DockerAssetApp(app, 'App2')); - - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', - expectedAssetRolePolicy('arn:*:iam::*:role/*-file-publishing-role-*', 'CdkAssetsFileRole6BE17A07')); - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', - expectedAssetRolePolicy('arn:*:iam::*:role/*-image-publishing-role-*', 'CdkAssetsDockerRole484B6DD3')); }); }); diff --git a/packages/@aws-cdk/pipelines/test/testutil.ts b/packages/@aws-cdk/pipelines/test/testutil.ts index 40135256b39d7..f3513eee6c5ce 100644 --- a/packages/@aws-cdk/pipelines/test/testutil.ts +++ b/packages/@aws-cdk/pipelines/test/testutil.ts @@ -2,7 +2,6 @@ import * as fs from 'fs'; import * as path from 'path'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; -import * as ec2 from '@aws-cdk/aws-ec2'; import * as s3 from '@aws-cdk/aws-s3'; import { App, AppProps, Environment, SecretValue, Stack, StackProps, Stage } from '@aws-cdk/core'; import { Construct } from 'constructs'; @@ -46,7 +45,6 @@ export class TestGitHubNpmPipeline extends cdkp.CdkPipeline { sourceArtifact, cloudAssemblyArtifact, }), - vpc: new ec2.Vpc(scope, 'TestVpc'), cloudAssemblyArtifact, ...props, }); diff --git a/packages/@aws-cdk/pipelines/test/validation.test.ts b/packages/@aws-cdk/pipelines/test/validation.test.ts index 4f1cffbef61ec..8cfe55d558b34 100644 --- a/packages/@aws-cdk/pipelines/test/validation.test.ts +++ b/packages/@aws-cdk/pipelines/test/validation.test.ts @@ -1,4 +1,4 @@ -import { anything, arrayWith, deepObjectLike, encodedJson } from '@aws-cdk/assert'; +import { anything, arrayWith, deepObjectLike, encodedJson, objectLike } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -389,6 +389,40 @@ test('run ShellScriptAction with specified BuildEnvironment', () => { }); }); +test('run ShellScriptAction with specified environment variables', () => { + // WHEN + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + actionName: 'imageAction', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + environmentVariables: { + VERSION: { value: codepipeline.GlobalVariables.executionId }, + }, + })); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Test', + Actions: [ + objectLike({ + Name: 'imageAction', + Configuration: objectLike({ + EnvironmentVariables: encodedJson([ + { + name: 'VERSION', + type: 'PLAINTEXT', + value: '#{codepipeline.PipelineExecutionId}', + }, + ]), + }), + }), + ], + }), + }); + +}); + class AppWithStackOutput extends Stage { public readonly output: CfnOutput; diff --git a/packages/@aws-cdk/yaml-cfn/package.json b/packages/@aws-cdk/yaml-cfn/package.json index a6ba44bac701b..7db67d8171bd4 100644 --- a/packages/@aws-cdk/yaml-cfn/package.json +++ b/packages/@aws-cdk/yaml-cfn/package.json @@ -67,7 +67,7 @@ }, "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "@types/yaml": "^1.9.7", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index ed98d3c1ccab4..6a6cca45a980c 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -34,14 +34,14 @@ "license": "Apache-2.0", "devDependencies": { "@monocdk-experiment/rewrite-imports": "0.0.0", - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "@types/node": "^10.17.51", "cdk-build-tools": "0.0.0", "constructs": "^3.2.0", "jest": "^26.6.3", "monocdk": "0.0.0", "pkglint": "0.0.0", - "ts-jest": "^26.4.4" + "ts-jest": "^26.5.1" }, "dependencies": { "@aws-cdk/cloudformation-diff": "0.0.0" diff --git a/packages/@monocdk-experiment/rewrite-imports/package.json b/packages/@monocdk-experiment/rewrite-imports/package.json index 18f0a6053f092..87cc7afc0f0aa 100644 --- a/packages/@monocdk-experiment/rewrite-imports/package.json +++ b/packages/@monocdk-experiment/rewrite-imports/package.json @@ -37,7 +37,7 @@ }, "devDependencies": { "@types/glob": "^7.1.3", - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "@types/node": "^10.17.51", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" diff --git a/packages/aws-cdk-lib/NOTICE b/packages/aws-cdk-lib/NOTICE index a2e13dfe49345..bd46bd848ec36 100644 --- a/packages/aws-cdk-lib/NOTICE +++ b/packages/aws-cdk-lib/NOTICE @@ -335,3 +335,40 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- + +** lru-cache - https://www.npmjs.com/package/lru-cache + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---------------- + +** yallist - https://www.npmjs.com/package/yallist + + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---------------- diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index 7c938b86bd7e0..cad68c582ab70 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -95,7 +95,7 @@ "jsonschema": "^1.4.0", "minimatch": "^3.0.4", "punycode": "^2.1.1", - "semver": "^7.3.2", + "semver": "^7.3.4", "yaml": "1.10.0" }, "devDependencies": { @@ -108,6 +108,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-authorizers": "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", @@ -202,6 +203,7 @@ "@aws-cdk/aws-kendra": "0.0.0", "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-kinesisanalytics": "0.0.0", + "@aws-cdk/aws-kinesisanalytics-flink": "0.0.0", "@aws-cdk/aws-kinesisfirehose": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-lakeformation": "0.0.0", @@ -213,6 +215,7 @@ "@aws-cdk/aws-licensemanager": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-logs-destinations": "0.0.0", + "@aws-cdk/aws-lookoutvision": "0.0.0", "@aws-cdk/aws-macie": "0.0.0", "@aws-cdk/aws-managedblockchain": "0.0.0", "@aws-cdk/aws-mediaconnect": "0.0.0", @@ -285,7 +288,7 @@ "constructs": "^3.2.0", "fs-extra": "^9.1.0", "pkglint": "0.0.0", - "ts-node": "^9.1.0", + "ts-node": "^9.1.1", "typescript": "~3.8.3", "ubergen": "0.0.0" }, diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index d17a38e62f923..78e2177b186cd 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -285,6 +285,18 @@ When `cdk deploy` is executed, deployment events will include the complete histo The `progress` key can also be specified as a user setting (`~/.cdk.json`) +#### Externally Executable CloudFormation Change Sets + +For more control over when stack changes are deployed, the CDK can generate a +CloudFormation change set but not execute it. The name of the generated +change set is *cdk-deploy-change-set*, and a previous change set with that +name will be overwritten. The change set will always be created, even if it +is empty. + +```console +$ cdk deploy --no-execute +``` + ### `cdk destroy` Deletes a stack from it's environment. This will cause the resources in the stack to be destroyed (unless they were diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index a5c6e04d39f1c..707c4ab770758 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -1,6 +1,5 @@ #!/usr/bin/env node import 'source-map-support/register'; - import * as cxapi from '@aws-cdk/cx-api'; import * as colors from 'colors/safe'; import * as yargs from 'yargs'; @@ -195,6 +194,10 @@ async function initCommandLine() { const cmd = argv._[0]; + if (typeof(cmd) !== 'string') { + throw new Error(`First argument should be a string. Got: ${cmd} (${typeof(cmd)})`); + } + // Bundle up global objects so the commands have access to them const commandOptions = { args: argv, configuration, aws: sdkProvider }; diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml index ee30856453ff9..b541401f930e7 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml @@ -358,7 +358,7 @@ Resources: Action: - ssm:GetParameter Resource: - - Fn::Sub: "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter${CdkBootstrapVersion}" + - Fn::Sub: "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${CdkBootstrapVersion}" Version: '2012-10-17' PolicyName: default RoleName: diff --git a/packages/aws-cdk/lib/api/cloudformation-deployments.ts b/packages/aws-cdk/lib/api/cloudformation-deployments.ts index 2eec90afdb790..3fe5fed118a76 100644 --- a/packages/aws-cdk/lib/api/cloudformation-deployments.ts +++ b/packages/aws-cdk/lib/api/cloudformation-deployments.ts @@ -282,10 +282,6 @@ export class CloudFormationDeployments { if (requiresBootstrapStackVersion === undefined) { return; } - if (!toolkitInfo.found) { - throw new Error(`${stackName}: publishing assets requires bootstrap stack version '${requiresBootstrapStackVersion}', no bootstrap stack found. Please run 'cdk bootstrap'.`); - } - try { await toolkitInfo.validateVersion(requiresBootstrapStackVersion, bootstrapStackVersionSsmParameter); } catch (e) { diff --git a/packages/aws-cdk/lib/api/cxapp/exec.ts b/packages/aws-cdk/lib/api/cxapp/exec.ts index 376ee542f4580..facaf24a3bfe0 100644 --- a/packages/aws-cdk/lib/api/cxapp/exec.ts +++ b/packages/aws-cdk/lib/api/cxapp/exec.ts @@ -92,7 +92,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom } async function exec() { - return new Promise((ok, fail) => { + return new Promise((ok, fail) => { // We use a slightly lower-level interface to: // // - Pass arguments in an array instead of a string, to get around a diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 1b52fb736e946..2dfb2f5119c71 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -174,6 +174,7 @@ export interface DeployStackOptions { } const LARGE_TEMPLATE_SIZE_KB = 50; +const CDK_CHANGE_SET_NAME = 'cdk-deploy-change-set'; /** @experimental */ export async function deployStack(options: DeployStackOptions): Promise { @@ -228,14 +229,20 @@ export async function deployStack(options: DeployStackOptions): Promise=3.0", "promptly": "^3.2.0", "proxy-agent": "^4.0.1", - "semver": "^7.3.2", + "semver": "^7.3.4", "source-map-support": "^0.5.19", "table": "^6.0.7", "uuid": "^8.3.2", diff --git a/packages/aws-cdk/test/api/bootstrap.test.ts b/packages/aws-cdk/test/api/bootstrap.test.ts index d04a49ec11472..888a649cc735e 100644 --- a/packages/aws-cdk/test/api/bootstrap.test.ts +++ b/packages/aws-cdk/test/api/bootstrap.test.ts @@ -49,6 +49,7 @@ beforeEach(() => { executed = true; return {}; }), + deleteChangeSet: jest.fn(), getTemplate: jest.fn(() => { executed = true; return {}; diff --git a/packages/aws-cdk/test/api/cloudformation-deployments.test.ts b/packages/aws-cdk/test/api/cloudformation-deployments.test.ts index 7afea33be6a0b..7a4e29628564c 100644 --- a/packages/aws-cdk/test/api/cloudformation-deployments.test.ts +++ b/packages/aws-cdk/test/api/cloudformation-deployments.test.ts @@ -76,7 +76,7 @@ test('deployment fails if bootstrap stack is missing', async () => { requiresBootstrapStackVersion: 99, }, }), - })).rejects.toThrow(/no bootstrap stack found/); + })).rejects.toThrow(/requires a bootstrap stack/); }); test('deployment fails if bootstrap stack is too old', async () => { diff --git a/packages/aws-cdk/test/api/deploy-stack.test.ts b/packages/aws-cdk/test/api/deploy-stack.test.ts index 1908b4716aac0..8fffb321ef995 100644 --- a/packages/aws-cdk/test/api/deploy-stack.test.ts +++ b/packages/aws-cdk/test/api/deploy-stack.test.ts @@ -459,6 +459,53 @@ test('not executed and no error if --no-execute is given', async () => { expect(cfnMocks.executeChangeSet).not.toHaveBeenCalled(); }); +test('empty change set is deleted if --execute is given', async () => { + cfnMocks.describeChangeSet?.mockImplementation(() => ({ + Status: 'FAILED', + StatusReason: 'No updates are to be performed.', + })); + + // GIVEN + givenStackExists(); + + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + execute: true, + force: true, // Necessary to bypass "skip deploy" + }); + + // THEN + expect(cfnMocks.createChangeSet).toHaveBeenCalled(); + expect(cfnMocks.executeChangeSet).not.toHaveBeenCalled(); + + //the first deletion is for any existing cdk change sets, the second is for the deleting the new empty change set + expect(cfnMocks.deleteChangeSet).toHaveBeenCalledTimes(2); +}); + +test('empty change set is not deleted if --no-execute is given', async () => { + cfnMocks.describeChangeSet?.mockImplementation(() => ({ + Status: 'FAILED', + StatusReason: 'No updates are to be performed.', + })); + + // GIVEN + givenStackExists(); + + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + execute: false, + }); + + // THEN + expect(cfnMocks.createChangeSet).toHaveBeenCalled(); + expect(cfnMocks.executeChangeSet).not.toHaveBeenCalled(); + + //the first deletion is for any existing cdk change sets + expect(cfnMocks.deleteChangeSet).toHaveBeenCalledTimes(1); +}); + test('use S3 url for stack deployment if present in Stack Artifact', async () => { // WHEN await deployStack({ diff --git a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts index e11e3b48bfcc0..c6ba2e53ec2d8 100644 --- a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts @@ -259,8 +259,9 @@ integTest('can deploy modern-synthesized stack even if bootstrap stack name is u // Deploy stack that uses file assets await fixture.cdkDeploy('lambda', { options: [ - // Next line explicitly commented to show that we don't pass it! - // '--toolkit-stack-name', bootstrapStackName, + // Explicity pass a name that's sure to not exist, otherwise the CLI might accidentally find a + // default bootstracp stack if that happens to be in the account already. + '--toolkit-stack-name', 'DefinitelyDoesNotExist', '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, '--context', '@aws-cdk/core:newStyleStackSynthesis=1', ], diff --git a/packages/aws-cdk/test/integ/github-helpers.ts b/packages/aws-cdk/test/integ/github-helpers.ts index 3a0bc1fb84195..10cff22158a8f 100644 --- a/packages/aws-cdk/test/integ/github-helpers.ts +++ b/packages/aws-cdk/test/integ/github-helpers.ts @@ -15,8 +15,8 @@ module.exports.fetchPreviousVersion = async function(base: string) { // this returns a list in decsending order, newest releases first for (const release of releases.data) { - const version = release.name.replace('v', ''); - if (semver.lt(version, base)) { + const version = release.name?.replace('v', ''); + if (version && semver.lt(version, base)) { return version; } } diff --git a/packages/aws-cdk/test/integ/uberpackage/cfn-include-app/example-template.json b/packages/aws-cdk/test/integ/uberpackage/cfn-include-app/example-template.json index 0385b58961413..8ad9310b8fd4d 100644 --- a/packages/aws-cdk/test/integ/uberpackage/cfn-include-app/example-template.json +++ b/packages/aws-cdk/test/integ/uberpackage/cfn-include-app/example-template.json @@ -1,5 +1,8 @@ { "Resources": { + "NoopHandle": { + "Type": "AWS::CloudFormation::WaitConditionHandle" + }, "Bucket": { "Type": "AWS::S3::Bucket", "Properties": { diff --git a/packages/awslint/package.json b/packages/awslint/package.json index 191a3bd6e24c2..b2f60b1dc12dd 100644 --- a/packages/awslint/package.json +++ b/packages/awslint/package.json @@ -16,16 +16,16 @@ "awslint": "bin/awslint" }, "dependencies": { - "@jsii/spec": "^1.15.0", + "@jsii/spec": "^1.20.1", "camelcase": "^6.2.0", "colors": "^1.4.0", "fs-extra": "^9.1.0", - "jsii-reflect": "^1.15.0", + "jsii-reflect": "^1.20.1", "yargs": "^16.2.0" }, "devDependencies": { "@types/fs-extra": "^8.1.1", - "@types/yargs": "^15.0.10", + "@types/yargs": "^15.0.13", "pkglint": "0.0.0", "typescript": "~3.9.7" }, diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index ba54b8625f279..87ddd5b672e30 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -32,11 +32,11 @@ "devDependencies": { "@types/archiver": "^5.1.0", "@types/glob": "^7.1.3", - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "@types/jszip": "^3.4.1", "@types/mock-fs": "^4.13.0", "@types/node": "^10.17.51", - "@types/yargs": "^15.0.10", + "@types/yargs": "^15.0.13", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "jszip": "^3.5.0", @@ -47,7 +47,7 @@ "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "archiver": "^5.2.0", - "aws-sdk": "^2.830.0", + "aws-sdk": "^2.842.0", "glob": "^7.1.6", "yargs": "^16.2.0" }, diff --git a/packages/cdk-dasm/package.json b/packages/cdk-dasm/package.json index ca73297f8f386..f17d6c6adaa90 100644 --- a/packages/cdk-dasm/package.json +++ b/packages/cdk-dasm/package.json @@ -26,11 +26,11 @@ }, "license": "Apache-2.0", "dependencies": { - "codemaker": "^1.15.0", + "codemaker": "^1.20.1", "yaml": "1.10.0" }, "devDependencies": { - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "@types/yaml": "1.9.7", "jest": "^26.6.3" }, diff --git a/packages/decdk/package.json b/packages/decdk/package.json index 7f8ace298e383..a7ca2a1d2ba1a 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -36,6 +36,7 @@ "@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-apigatewayv2-authorizers": "0.0.0", "@aws-cdk/aws-appconfig": "0.0.0", "@aws-cdk/aws-appflow": "0.0.0", "@aws-cdk/aws-applicationautoscaling": "0.0.0", @@ -128,6 +129,7 @@ "@aws-cdk/aws-kendra": "0.0.0", "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-kinesisanalytics": "0.0.0", + "@aws-cdk/aws-kinesisanalytics-flink": "0.0.0", "@aws-cdk/aws-kinesisfirehose": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-lakeformation": "0.0.0", @@ -139,6 +141,7 @@ "@aws-cdk/aws-licensemanager": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-logs-destinations": "0.0.0", + "@aws-cdk/aws-lookoutvision": "0.0.0", "@aws-cdk/aws-macie": "0.0.0", "@aws-cdk/aws-managedblockchain": "0.0.0", "@aws-cdk/aws-mediaconnect": "0.0.0", @@ -207,18 +210,18 @@ "@aws-cdk/yaml-cfn": "0.0.0", "constructs": "^3.2.0", "fs-extra": "^9.1.0", - "jsii-reflect": "^1.15.0", + "jsii-reflect": "^1.20.1", "jsonschema": "^1.4.0", "yaml": "1.10.0", "yargs": "^16.2.0" }, "devDependencies": { "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "@types/yaml": "1.9.7", - "@types/yargs": "^15.0.10", + "@types/yargs": "^15.0.13", "jest": "^26.6.3", - "jsii": "^1.15.0" + "jsii": "^1.20.1" }, "keywords": [ "aws", diff --git a/packages/monocdk/NOTICE b/packages/monocdk/NOTICE index a2e13dfe49345..bd46bd848ec36 100644 --- a/packages/monocdk/NOTICE +++ b/packages/monocdk/NOTICE @@ -335,3 +335,40 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------- + +** lru-cache - https://www.npmjs.com/package/lru-cache + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---------------- + +** yallist - https://www.npmjs.com/package/yallist + + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---------------- diff --git a/packages/monocdk/package.json b/packages/monocdk/package.json index dddf5eb0b77fd..33e824037bb3c 100644 --- a/packages/monocdk/package.json +++ b/packages/monocdk/package.json @@ -100,7 +100,7 @@ "jsonschema": "^1.4.0", "minimatch": "^3.0.4", "punycode": "^2.1.1", - "semver": "^7.3.2", + "semver": "^7.3.4", "yaml": "1.10.0" }, "devDependencies": { @@ -113,6 +113,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-authorizers": "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", @@ -207,6 +208,7 @@ "@aws-cdk/aws-kendra": "0.0.0", "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-kinesisanalytics": "0.0.0", + "@aws-cdk/aws-kinesisanalytics-flink": "0.0.0", "@aws-cdk/aws-kinesisfirehose": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-lakeformation": "0.0.0", @@ -218,6 +220,7 @@ "@aws-cdk/aws-licensemanager": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-logs-destinations": "0.0.0", + "@aws-cdk/aws-lookoutvision": "0.0.0", "@aws-cdk/aws-macie": "0.0.0", "@aws-cdk/aws-managedblockchain": "0.0.0", "@aws-cdk/aws-mediaconnect": "0.0.0", @@ -290,7 +293,7 @@ "constructs": "^3.2.0", "fs-extra": "^9.1.0", "pkglint": "0.0.0", - "ts-node": "^9.1.0", + "ts-node": "^9.1.1", "typescript": "~3.8.3", "ubergen": "0.0.0" }, diff --git a/tools/cdk-build-tools/config/eslintrc.js b/tools/cdk-build-tools/config/eslintrc.js index 446af2c2e2ff4..8fda93acca482 100644 --- a/tools/cdk-build-tools/config/eslintrc.js +++ b/tools/cdk-build-tools/config/eslintrc.js @@ -41,6 +41,7 @@ module.exports = { }, ignorePatterns: ['*.js', '*.d.ts', 'node_modules/', '*.generated.ts'], rules: { + 'cdk/construct-import-order': [ 'error' ], 'cdk/no-core-construct': [ 'error' ], 'cdk/no-qualified-construct': [ 'error' ], // Require use of the `import { foo } from 'bar';` form instead of `import foo = require('bar');` diff --git a/tools/cdk-build-tools/lib/feature-flag.ts b/tools/cdk-build-tools/lib/feature-flag.ts new file mode 100644 index 0000000000000..eec1d28d6e9e4 --- /dev/null +++ b/tools/cdk-build-tools/lib/feature-flag.ts @@ -0,0 +1,81 @@ +import * as path from 'path'; +import * as fs from 'fs-extra'; +import * as semver from 'semver'; + +/* eslint-disable jest/no-export */ + +type Flags = {[key: string]: any}; + +/** + * jest helper function to be used when testing future flags. Twin function of the `testLegacyBehavior()`. + * This should be used for testing future flags that will be removed in CDKv2, and updated such that these + * will be the default behaviour. + * + * This function is specifically for unit tests that verify the behaviour when future flags are enabled. + * + * The version of CDK is determined by running `scripts/resolve-version.js`, and the logic is as follows: + * + * When run in CDKv1, the specified 'flags' parameter are passed into the CDK App's context, and then + * the test is executed. + * + * When run in CDKv2, the specified 'flags' parameter is ignored, since the default behaviour should be as if + * they are enabled, and then the test is executed. + */ +export function testFutureBehavior( + name: string, + flags: Flags, + cdkApp: new (props?: { context: Flags }) => T, + fn: (app: T) => void, + repoRoot: string = path.join(process.cwd(), '..', '..', '..')) { + + const major = cdkMajorVersion(repoRoot); + if (major === 2) { + // Temporaily disable CDKv2 behaviour + // const app = new cdkApp(); + // return test(name, async () => fn(app)); + } + const app = new cdkApp({ context: flags }); + return test(name, () => fn(app)); +} + +/** + * jest helper function to be used when testing future flags. Twin function of the `testFutureBehavior()`. + * This should be used for testing future flags that will be removed in CDKv2, and updated such that these + * will be the default behaviour. + * + * This function is specifically for unit tests that verify the behaviour when future flags are disabled. + * + * The version of CDK is determined by running `scripts/resolve-version.js`, and the logic is as follows: + * + * When run in CDKv1, the test is executed as normal. + * + * When run in CDKv2, the test is skipped, since the feature flag usage is unsupported and blocked. + */ +export function testLegacyBehavior( + name: string, + cdkApp: new () => T, + fn: (app: T) => void, + repoRoot: string = path.join(process.cwd(), '..', '..', '..')) { + + const major = cdkMajorVersion(repoRoot); + if (major === 2) { + // Temporarily disable CDKv2 behaviour + // return; + } + const app = new cdkApp(); + return test(name, () => fn(app)); +} + +function cdkMajorVersion(repoRoot: string) { + const resolveVersionPath = path.join(repoRoot, 'scripts', 'resolve-version.js'); + if (!fs.existsSync(resolveVersionPath)) { + throw new Error(`file not present at path ${resolveVersionPath}. You will likely need to set 'repoRoot'.`); + } + // eslint-disable-next-line @typescript-eslint/no-require-imports + const ver = require(resolveVersionPath).version; + const sem = semver.parse(ver); + if (!sem) { + throw new Error(`version ${ver} is not a semver`); + } + return sem.major; +} \ No newline at end of file diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 68333bedf2f90..aa77ccb240bc6 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -34,16 +34,16 @@ "license": "Apache-2.0", "devDependencies": { "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.15", - "@types/yargs": "^15.0.10", + "@types/jest": "^26.0.20", + "@types/yargs": "^15.0.13", "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^4.14.0", - "@typescript-eslint/parser": "^4.7.0", + "@typescript-eslint/eslint-plugin": "^4.15.0", + "@typescript-eslint/parser": "^4.14.2", "awslint": "0.0.0", "colors": "^1.4.0", - "eslint": "^7.13.0", + "eslint": "^7.19.0", "eslint-import-resolver-node": "^0.3.4", "eslint-import-resolver-typescript": "^2.3.0", "eslint-plugin-cdk": "0.0.0", @@ -51,12 +51,13 @@ "eslint-plugin-jest": "^24.1.3", "fs-extra": "^9.1.0", "jest": "^26.6.3", - "jsii": "^1.15.0", - "jsii-pacmak": "^1.15.0", + "jsii": "^1.20.1", + "jsii-pacmak": "^1.20.1", "markdownlint-cli": "^0.26.0", "nodeunit": "^0.11.3", "nyc": "^15.1.0", - "ts-jest": "^26.4.4", + "semver": "^7.3.4", + "ts-jest": "^26.5.1", "typescript": "~3.9.7", "yargs": "^16.2.0", "yarn-cling": "0.0.0" diff --git a/tools/cdk-integ-tools/bin/cdk-integ.ts b/tools/cdk-integ-tools/bin/cdk-integ.ts index 49110c8dccb17..30afa7c8102b9 100644 --- a/tools/cdk-integ-tools/bin/cdk-integ.ts +++ b/tools/cdk-integ-tools/bin/cdk-integ.ts @@ -14,7 +14,7 @@ async function main() { .option('dry-run', { type: 'boolean', default: false, desc: 'do not actually deploy the stack. just update the snapshot (not recommended!)' }) .argv; - const tests = await new IntegrationTests('test').fromCliArgs(argv._); + const tests = await new IntegrationTests('test').fromCliArgs(argv._.map(x => x.toString())); if (argv.list) { process.stdout.write(tests.map(t => t.name).join(' ') + '\n'); diff --git a/tools/cdk-integ-tools/package.json b/tools/cdk-integ-tools/package.json index e1eb7d6946a65..bb9fbacb970b0 100644 --- a/tools/cdk-integ-tools/package.json +++ b/tools/cdk-integ-tools/package.json @@ -30,7 +30,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/fs-extra": "^8.1.1", - "@types/yargs": "^15.0.10", + "@types/yargs": "^15.0.13", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" }, diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index f1e9f184e67af..08d0636a841f5 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -684,6 +684,15 @@ export default class CodeGenerator { this.code.line(`const errors = new ${CORE}.ValidationResults();`); + // check that the argument is an object + // normally, we would have to explicitly check for null here, + // as typeof null is 'object' in JavaScript, + // but validators are never called with null + // (as evidenced by the code below accessing properties of the argument without checking for null) + this.code.openBlock("if (typeof properties !== 'object')"); + this.code.line(`errors.collect(new ${CORE}.ValidationResult('Expected an object, but received: ' + JSON.stringify(properties)));`); + this.code.closeBlock(); + Object.keys(propSpecs).forEach(cfnPropName => { const propSpec = propSpecs[cfnPropName]; const propName = nameConversionTable[cfnPropName]; @@ -898,7 +907,7 @@ export default class CodeGenerator { this.code.line('/**'); before.forEach(line => this.code.line(` * ${line}`.trimRight())); if (link) { - this.code.line(` * @see ${link}`); + this.code.line(` * @link ${link}`); } this.code.line(' */'); return; diff --git a/tools/cfn2ts/package.json b/tools/cfn2ts/package.json index c97edb5b5d470..0a4297e691b39 100644 --- a/tools/cfn2ts/package.json +++ b/tools/cfn2ts/package.json @@ -30,15 +30,15 @@ "license": "Apache-2.0", "dependencies": { "@aws-cdk/cfnspec": "0.0.0", - "codemaker": "^1.15.0", + "codemaker": "^1.20.1", "fast-json-patch": "^3.0.0-1", "fs-extra": "^9.1.0", "yargs": "^16.2.0" }, "devDependencies": { "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.15", - "@types/yargs": "^15.0.10", + "@types/jest": "^26.0.20", + "@types/yargs": "^15.0.13", "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0" diff --git a/tools/eslint-plugin-cdk/lib/index.ts b/tools/eslint-plugin-cdk/lib/index.ts index 94eef4dd8f57f..a9eb84b77ef0f 100644 --- a/tools/eslint-plugin-cdk/lib/index.ts +++ b/tools/eslint-plugin-cdk/lib/index.ts @@ -1,4 +1,5 @@ export const rules = { 'no-core-construct': require('./rules/no-core-construct'), 'no-qualified-construct': require('./rules/no-qualified-construct'), + 'construct-import-order': require('./rules/construct-import-order'), }; diff --git a/tools/eslint-plugin-cdk/lib/rules/construct-import-order.ts b/tools/eslint-plugin-cdk/lib/rules/construct-import-order.ts new file mode 100644 index 0000000000000..fc6066e486747 --- /dev/null +++ b/tools/eslint-plugin-cdk/lib/rules/construct-import-order.ts @@ -0,0 +1,104 @@ +// +// This rule ensures that the `@aws-cdk/core.Construct` class is always +// imported at the end, and in a separate section. In the `v2-main` branch, +// this class is removed and so is the import. Keeping it in a separate line +// and section ensures that any other adjustments to the import do not cause +// conflicts on forward merges. +// + +import { ImportDeclaration } from 'estree'; +import { Rule } from 'eslint'; + +interface ImportOrderViolation { + node: ImportDeclaration; + localName: string; + range: [number, number]; +} + +let importOrderViolation: ImportOrderViolation | undefined; +let coreConstructImportLine: ImportDeclaration | undefined; +let lastImport: Rule.Node | undefined; + +export function create(context: Rule.RuleContext): Rule.NodeListener { + return { + Program: _ => { + // reset for every file + importOrderViolation = undefined; + coreConstructImportLine = undefined; + lastImport = undefined; + }, + + // collect all "import" statements. we will later use them to determine + // exactly how to import `core.Construct`. + ImportDeclaration: node => { + lastImport = node; + + if (coreConstructImportLine && coreConstructImportLine.range) { + // If CoreConstruct import was previously seen, this import line should not succeed it. + + importOrderViolation = { + node: coreConstructImportLine, + range: coreConstructImportLine.range, + localName: coreConstructImportLine.specifiers[0].local.name, + }; + } + + for (const [i, s] of node.specifiers.entries()) { + const isConstruct = (s.local.name === 'CoreConstruct' || s.local.name === 'Construct') && node.source.value === '@aws-cdk/core'; + if (isConstruct && s.range) { + if (node.specifiers.length > 1) { + // if there is more than one specifier on the line that also imports CoreConstruct, i.e., + // `import { Resource, Construct as CoreConstruct, Token } from '@aws-cdk/core'` + + // If this is the last specifier, delete just that. If not, delete until the beginning of the next specifier. + const range: [number, number] = (i === node.specifiers.length - 1) ? s.range : [s.range[0], node.specifiers[i + 1].range![0]]; + importOrderViolation = { node, range, localName: s.local.name }; + } else { + // This means that CoreConstruct is the only import within this line, + // so record the node so the whole line can be removed if there are imports that follow + + coreConstructImportLine = node; + } + } + } + }, + + Identifier: node => { + if ( + node.parent.type !== 'ImportSpecifier' && + (node.name === 'CoreConstruct' || node.name === 'Construct') && + importOrderViolation + ) { + reportImportOrderViolations(context); + } + }, + } +} + +function reportImportOrderViolations(context: Rule.RuleContext) { + if (importOrderViolation && lastImport) { + const violation = importOrderViolation; + const _lastImport = lastImport; + context.report({ + message: 'To avoid merge conflicts with the v2 branch, import of "@aws-cdk/core.Construct" must be in its own line, ' + + 'and as the very last import.', + node: violation.node, + fix: fixer => { + const fixes: Rule.Fix[] = []; + fixes.push(fixer.removeRange(violation.range)); + const sym = violation.localName === 'Construct' ? 'Construct' : 'Construct as CoreConstruct' + const addImport = `import { ${sym} } from '@aws-cdk/core';`; + fixes.push(fixer.insertTextAfter(_lastImport, [ + "", + "", + "// keep this import separate from other imports to reduce chance for merge conflicts with v2-main", + "// eslint-disable-next-line no-duplicate-imports, import/order", + addImport, + ].join('\n'))); + return fixes; + } + }); + // reset, so that this is reported only once + importOrderViolation = undefined; + } +} \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/package.json b/tools/eslint-plugin-cdk/package.json index ae52785ea619a..e92e010ef1ca5 100644 --- a/tools/eslint-plugin-cdk/package.json +++ b/tools/eslint-plugin-cdk/package.json @@ -14,15 +14,15 @@ "devDependencies": { "@types/eslint": "^7.2.6", "@types/fs-extra": "^8.1.1", - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "@types/node": "^10.17.51", "eslint-plugin-rulesdir": "^0.1.0", "jest": "^26.6.3", "typescript": "~3.9.7" }, "dependencies": { - "@typescript-eslint/parser": "^4.7.0", - "eslint": "^7.13.0", + "@typescript-eslint/parser": "^4.14.2", + "eslint": "^7.19.0", "fs-extra": "^9.1.0" }, "jest": { diff --git a/tools/eslint-plugin-cdk/test/rules/eslintrc.js b/tools/eslint-plugin-cdk/test/rules/eslintrc.js deleted file mode 100644 index c68b2066acce3..0000000000000 --- a/tools/eslint-plugin-cdk/test/rules/eslintrc.js +++ /dev/null @@ -1,12 +0,0 @@ -const path = require('path'); -const rulesDirPlugin = require('eslint-plugin-rulesdir'); -rulesDirPlugin.RULES_DIR = path.join(__dirname, '../../lib/rules'); - -module.exports = { - parser: '@typescript-eslint/parser', - plugins: ['rulesdir'], - rules: { - quotes: [ 'error', 'single', { avoidEscape: true }], - 'rulesdir/no-core-construct': [ 'error' ], - } -} diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures.test.ts b/tools/eslint-plugin-cdk/test/rules/fixtures.test.ts new file mode 100644 index 0000000000000..bf1c03f359047 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures.test.ts @@ -0,0 +1,69 @@ +import { ESLint } from 'eslint'; +import * as fs from 'fs-extra'; +import * as path from 'path'; + +const rulesDirPlugin = require('eslint-plugin-rulesdir'); +rulesDirPlugin.RULES_DIR = path.join(__dirname, '../../lib/rules'); + +let linter: ESLint; + +const outputRoot = path.join(process.cwd(), '.test-output'); +fs.mkdirpSync(outputRoot); + +const fixturesRoot = path.join(__dirname, 'fixtures'); + +fs.readdirSync(fixturesRoot).filter(f => fs.lstatSync(path.join(fixturesRoot, f)).isDirectory()).forEach(d => { + describe(d, () => { + const fixturesDir = path.join(fixturesRoot, d); + + beforeAll(() => { + linter = new ESLint({ + baseConfig: { + parser: '@typescript-eslint/parser', + }, + overrideConfigFile: path.join(fixturesDir, 'eslintrc.js'), + rulePaths: [ + path.join(__dirname, '../../lib/rules'), + ], + fix: true, + }); + }); + + const outputDir = path.join(outputRoot, d); + fs.mkdirpSync(outputDir); + + const fixtureFiles = fs.readdirSync(fixturesDir).filter(f => f.endsWith('.ts') && !f.endsWith('.expected.ts')); + + fixtureFiles.forEach(f => { + test(f, async (done) => { + const actualFile = await lintAndFix(path.join(fixturesDir, f), outputDir); + const expectedFile = path.join(fixturesDir, `${path.basename(f, '.ts')}.expected.ts`); + if (!fs.existsSync(expectedFile)) { + done.fail(`Expected file not found. Generated output at ${actualFile}`); + } + const actual = await fs.readFile(actualFile, { encoding: 'utf8' }); + const expected = await fs.readFile(expectedFile, { encoding: 'utf8' }); + if (actual !== expected) { + done.fail(`Linted file did not match expectations. Expected: ${expectedFile}. Actual: ${actualFile}`); + } + done(); + }); + }); + }); +}); + +async function lintAndFix(file: string, outputDir: string) { + const newPath = path.join(outputDir, path.basename(file)) + let result = await linter.lintFiles(file); + const hasFixes = result.find(r => typeof(r.output) === 'string') !== undefined; + if (hasFixes) { + await ESLint.outputFixes(result.map(r => { + r.filePath = newPath; + return r; + })); + } else { + // If there are no fixes, copy the input file as output + await fs.copyFile(file, newPath); + } + return newPath; +} diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-class-usage.expected.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-class-usage.expected.ts new file mode 100644 index 0000000000000..a6c9ecede29f0 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-class-usage.expected.ts @@ -0,0 +1,9 @@ + +import { Something } from 'Somewhere'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +class MyConstruct extends Construct { +} \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-class-usage.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-class-usage.ts new file mode 100644 index 0000000000000..8689366792602 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-class-usage.ts @@ -0,0 +1,5 @@ +import { Construct } from '@aws-cdk/core'; +import { Something } from 'Somewhere'; + +class MyConstruct extends Construct { +} \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-multiple-specifiers.expected.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-multiple-specifiers.expected.ts new file mode 100644 index 0000000000000..b0458a380f220 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-multiple-specifiers.expected.ts @@ -0,0 +1,8 @@ +import { Resource } from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +let x: Resource; +let y: Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-multiple-specifiers.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-multiple-specifiers.ts new file mode 100644 index 0000000000000..c417c728ab86c --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-multiple-specifiers.ts @@ -0,0 +1,4 @@ +import { Construct, Resource } from '@aws-cdk/core'; + +let x: Resource; +let y: Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-nonfinal.expected.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-nonfinal.expected.ts new file mode 100644 index 0000000000000..a9e29b0d3a912 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-nonfinal.expected.ts @@ -0,0 +1,9 @@ + +import { Something } from 'Somewhere'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +let x: Something; +let y: Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-nonfinal.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-nonfinal.ts new file mode 100644 index 0000000000000..d7716d82c3d12 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/construct-nonfinal.ts @@ -0,0 +1,5 @@ +import { Construct } from '@aws-cdk/core'; +import { Something } from 'Somewhere'; + +let x: Something; +let y: Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-class-usage.expected.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-class-usage.expected.ts new file mode 100644 index 0000000000000..eccef051ab56c --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-class-usage.expected.ts @@ -0,0 +1,9 @@ + +import { Something } from 'Somewhere'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +class MyConstruct extends CoreConstruct { +} \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-class-usage.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-class-usage.ts new file mode 100644 index 0000000000000..5cf72993ecbe9 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-class-usage.ts @@ -0,0 +1,5 @@ +import { Construct as CoreConstruct } from '@aws-cdk/core'; +import { Something } from 'Somewhere'; + +class MyConstruct extends CoreConstruct { +} \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-multiple-specifiers.expected.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-multiple-specifiers.expected.ts new file mode 100644 index 0000000000000..8a8b9c6efe4b4 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-multiple-specifiers.expected.ts @@ -0,0 +1,9 @@ +import { Resource, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs' + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +let x: CoreConstruct; +let y: Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-multiple-specifiers.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-multiple-specifiers.ts new file mode 100644 index 0000000000000..403f5166a31bc --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-multiple-specifiers.ts @@ -0,0 +1,5 @@ +import { Construct as CoreConstruct, Resource, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs' + +let x: CoreConstruct; +let y: Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-nonfinal.expected.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-nonfinal.expected.ts new file mode 100644 index 0000000000000..a0430db275df2 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-nonfinal.expected.ts @@ -0,0 +1,9 @@ + +import { Construct } from 'constructs'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +let x: CoreConstruct; +let y: Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-nonfinal.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-nonfinal.ts new file mode 100644 index 0000000000000..98baab61bb84c --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/coreconstruct-nonfinal.ts @@ -0,0 +1,5 @@ +import { Construct as CoreConstruct } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +let x: CoreConstruct; +let y: Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/eslintrc.js b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/eslintrc.js new file mode 100644 index 0000000000000..3082e03d4ed79 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/construct-import-order/eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['rulesdir'], + rules: { + 'rulesdir/construct-import-order': [ 'error' ], + } +} \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-core-construct/eslintrc.js b/tools/eslint-plugin-cdk/test/rules/fixtures/no-core-construct/eslintrc.js new file mode 100644 index 0000000000000..3bd78e797f728 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-core-construct/eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['rulesdir'], + rules: { + 'rulesdir/no-core-construct': [ 'error' ], + } +} \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/both-constructs.expected.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/both-constructs.expected.ts new file mode 100644 index 0000000000000..20caf8244cd1b --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/both-constructs.expected.ts @@ -0,0 +1,9 @@ +import { Construct } from 'constructs' +import * as cdk from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +let x: CoreConstruct; +let y: Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/both-constructs.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/both-constructs.ts new file mode 100644 index 0000000000000..bd92a909af763 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/both-constructs.ts @@ -0,0 +1,5 @@ +import { Construct } from 'constructs' +import * as cdk from '@aws-cdk/core'; + +let x: cdk.Construct; +let y: Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/eslintrc.js b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/eslintrc.js new file mode 100644 index 0000000000000..30de0b87a63df --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['rulesdir'], + rules: { + 'rulesdir/no-qualified-construct': [ 'error' ], + } +} \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-heritage.expected.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-heritage.expected.ts new file mode 100644 index 0000000000000..6510d7dd5542f --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-heritage.expected.ts @@ -0,0 +1,8 @@ +import * as cdk from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +class MyConstruct extends Construct { +} \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-heritage.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-heritage.ts new file mode 100644 index 0000000000000..3f8b877e32c2e --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-heritage.ts @@ -0,0 +1,4 @@ +import * as cdk from '@aws-cdk/core'; + +class MyConstruct extends cdk.Construct { +} \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.expected.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.expected.ts new file mode 100644 index 0000000000000..bba5c3ae8aa50 --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.expected.ts @@ -0,0 +1,7 @@ +import * as cdk from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +let x: Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.ts b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.ts new file mode 100644 index 0000000000000..d2ebc12dc01ff --- /dev/null +++ b/tools/eslint-plugin-cdk/test/rules/fixtures/no-qualified-construct/qualified-usage.ts @@ -0,0 +1,3 @@ +import * as cdk from '@aws-cdk/core'; + +let x: cdk.Construct; \ No newline at end of file diff --git a/tools/eslint-plugin-cdk/test/rules/no-core-construct.test.ts b/tools/eslint-plugin-cdk/test/rules/no-core-construct.test.ts deleted file mode 100644 index c2272cfd39353..0000000000000 --- a/tools/eslint-plugin-cdk/test/rules/no-core-construct.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ESLint } from 'eslint'; -import * as fs from 'fs-extra'; -import * as path from 'path'; - -const linter = new ESLint({ - overrideConfigFile: path.join(__dirname, 'eslintrc.js'), - rulePaths: [ - path.join(__dirname, '../../lib/rules'), - ], - fix: true, -}); - -const outputDir = path.join(process.cwd(), '.test-output'); -fs.mkdirpSync(outputDir); -const fixturesDir = path.join(__dirname, 'fixtures', 'no-core-construct'); - -describe('no-core-construct', () => { - const fixtureFiles = fs.readdirSync(fixturesDir).filter(f => f.endsWith('.ts') && !f.endsWith('.expected.ts')); - fixtureFiles.forEach(f => { - test(f, async (done) => { - const actualFile = await lintAndFix(path.join(fixturesDir, f)); - const expectedFile = path.join(fixturesDir, `${path.basename(f, '.ts')}.expected.ts`); - if (!fs.existsSync(expectedFile)) { - done.fail(`Expected file not found. Generated output at ${actualFile}`); - } - const actual = await fs.readFile(actualFile, { encoding: 'utf8' }); - const expected = await fs.readFile(expectedFile, { encoding: 'utf8' }); - if (actual !== expected) { - done.fail(`Linted file did not match expectations. Expected: ${expectedFile}. Actual: ${actualFile}`); - } - done(); - }); - }); -}); - -async function lintAndFix(file: string) { - const newPath = path.join(outputDir, path.basename(file)) - let result = await linter.lintFiles(file); - await ESLint.outputFixes(result.map(r => { - r.filePath = newPath; - return r; - })); - return newPath; -} diff --git a/tools/nodeunit-shim/package.json b/tools/nodeunit-shim/package.json index 3aa95f20e3dfc..675f0cbf3e649 100644 --- a/tools/nodeunit-shim/package.json +++ b/tools/nodeunit-shim/package.json @@ -12,7 +12,7 @@ "build+test": "npm run build && npm test" }, "devDependencies": { - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "@types/node": "^10.17.51", "typescript": "~3.9.7" }, diff --git a/tools/pkglint/bin/pkglint.ts b/tools/pkglint/bin/pkglint.ts index 625c8ab00223f..0b5cd61ef1649 100644 --- a/tools/pkglint/bin/pkglint.ts +++ b/tools/pkglint/bin/pkglint.ts @@ -12,13 +12,17 @@ const argv = yargs // Our version of yargs doesn't support positional arguments yet const directory = argv._[0] || '.'; +if (typeof(directory) !== 'string') { + throw new Error(`First argument should be a string. Got: ${directory} (${typeof(directory)})`); +} + argv.directory = path.resolve(directory, process.cwd()); async function main(): Promise { const ruleClasses = require('../lib/rules'); // eslint-disable-line @typescript-eslint/no-require-imports const rules: ValidationRule[] = Object.keys(ruleClasses).map(key => new ruleClasses[key]()).filter(obj => obj instanceof ValidationRule); - const pkgs = findPackageJsons(directory); + const pkgs = findPackageJsons(argv.directory as string); rules.forEach(rule => pkgs.filter(pkg => pkg.shouldApply(rule)).forEach(pkg => rule.prepare(pkg))); rules.forEach(rule => pkgs.filter(pkg => pkg.shouldApply(rule)).forEach(pkg => rule.validate(pkg))); @@ -27,7 +31,7 @@ async function main(): Promise { pkgs.forEach(pkg => pkg.applyFixes()); } - pkgs.forEach(pkg => pkg.displayReports(directory)); + pkgs.forEach(pkg => pkg.displayReports(argv.directory as string)); if (pkgs.some(p => p.hasReports)) { throw new Error('Some package.json files had errors'); diff --git a/tools/pkglint/lib/rules.ts b/tools/pkglint/lib/rules.ts index 4b3c88eab01cd..0d75c3b1113cf 100644 --- a/tools/pkglint/lib/rules.ts +++ b/tools/pkglint/lib/rules.ts @@ -1070,11 +1070,7 @@ export class MustHaveNodeEnginesDeclaration extends ValidationRule { public readonly name = 'package-info/engines'; public validate(pkg: PackageJson): void { - if (cdkMajorVersion() === 2) { - expectJSON(this.name, pkg, 'engines.node', '>= 14.15.0'); - } else { - expectJSON(this.name, pkg, 'engines.node', '>= 10.13.0 <13 || >=13.7.0'); - } + expectJSON(this.name, pkg, 'engines.node', '>= 10.13.0 <13 || >=13.7.0'); } } diff --git a/tools/pkglint/package.json b/tools/pkglint/package.json index 5272d5a1b1508..c4bbd7e87c0ba 100644 --- a/tools/pkglint/package.json +++ b/tools/pkglint/package.json @@ -37,7 +37,7 @@ "devDependencies": { "@types/fs-extra": "^8.1.1", "@types/semver": "^7.3.4", - "@types/yargs": "^15.0.10", + "@types/yargs": "^15.0.13", "eslint-plugin-cdk": "0.0.0", "jest": "^26.6.3", "typescript": "~3.9.7" @@ -48,7 +48,7 @@ "fs-extra": "^9.1.0", "glob": "^7.1.6", "npm-bundled": "^1.1.1", - "semver": "^7.3.2", + "semver": "^7.3.4", "yargs": "^16.2.0" } } diff --git a/tools/pkgtools/package.json b/tools/pkgtools/package.json index 556178ccfd165..18b4ef85db565 100644 --- a/tools/pkgtools/package.json +++ b/tools/pkgtools/package.json @@ -30,7 +30,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/fs-extra": "^8.1.1", - "@types/yargs": "^15.0.10", + "@types/yargs": "^15.0.13", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" }, diff --git a/tools/prlint/package.json b/tools/prlint/package.json index 22a7fd221163d..c9e6e895d56d4 100644 --- a/tools/prlint/package.json +++ b/tools/prlint/package.json @@ -11,11 +11,11 @@ }, "license": "Apache-2.0", "dependencies": { - "github-api": "^3.3.0" + "github-api": "^3.4.0" }, "scripts": { "build": "echo success", - "test": "echo sucees", + "test": "echo success", "build+test": "npm run build test && npm run test" } } diff --git a/tools/ubergen/bin/ubergen.ts b/tools/ubergen/bin/ubergen.ts index ea5c529f77f3e..9047fc28c371a 100644 --- a/tools/ubergen/bin/ubergen.ts +++ b/tools/ubergen/bin/ubergen.ts @@ -333,8 +333,11 @@ async function copyOrTransformFiles(from: string, to: string, libraries: readonl const cfnTypes2Classes: { [key: string]: string } = await fs.readJson(source); for (const cfnType of Object.keys(cfnTypes2Classes)) { const fqn = cfnTypes2Classes[cfnType]; - // replace @aws-cdk/aws- with /aws- - cfnTypes2Classes[cfnType] = fqn.replace('@aws-cdk', uberPackageJson.name); + // replace @aws-cdk/aws- with /aws-, + // except for @aws-cdk/core, which maps just to the name of the uberpackage + cfnTypes2Classes[cfnType] = fqn.startsWith('@aws-cdk/core.') + ? fqn.replace('@aws-cdk/core', uberPackageJson.name) + : fqn.replace('@aws-cdk', uberPackageJson.name); } await fs.writeJson(destination, cfnTypes2Classes, { spaces: 2 }); } else { diff --git a/tools/yarn-cling/package.json b/tools/yarn-cling/package.json index 421d3314e0400..6cc21f56b710d 100644 --- a/tools/yarn-cling/package.json +++ b/tools/yarn-cling/package.json @@ -38,7 +38,7 @@ ] }, "devDependencies": { - "@types/jest": "^26.0.15", + "@types/jest": "^26.0.20", "@types/node": "^10.17.51", "@types/yarnpkg__lockfile": "^1.1.4", "jest": "^26.6.3", diff --git a/version.v1.json b/version.v1.json index 549ac5ace19cd..345b477275b1d 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.86.0" + "version": "1.89.0" } diff --git a/yarn.lock b/yarn.lock index 04ec4d7447fea..8edfd60d28959 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,152 +2,151 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" - integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" + integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== dependencies: - "@babel/highlight" "^7.10.4" + "@babel/highlight" "^7.12.13" "@babel/core@^7.1.0", "@babel/core@^7.7.5": - version "7.12.3" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.3.tgz#1b436884e1e3bff6fb1328dc02b208759de92ad8" - integrity sha512-0qXcZYKZp3/6N2jKYVxZv0aNCsxTSVCiK72DTiTYZAu7sjg73W0/aynWjMbiGd87EQL4WyA8reiJVh92AVla9g== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.12.1" - "@babel/helper-module-transforms" "^7.12.1" - "@babel/helpers" "^7.12.1" - "@babel/parser" "^7.12.3" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.12.1" - "@babel/types" "^7.12.1" + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.13.tgz#b73a87a3a3e7d142a66248bf6ad88b9ceb093425" + integrity sha512-BQKE9kXkPlXHPeqissfxo0lySWJcYdEP0hdtJOH/iJfDdhOCcgtNCjftCJg3qqauB4h+lz2N6ixM++b9DN1Tcw== + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/generator" "^7.12.13" + "@babel/helper-module-transforms" "^7.12.13" + "@babel/helpers" "^7.12.13" + "@babel/parser" "^7.12.13" + "@babel/template" "^7.12.13" + "@babel/traverse" "^7.12.13" + "@babel/types" "^7.12.13" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" json5 "^2.1.2" lodash "^4.17.19" - resolve "^1.3.2" semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.12.1", "@babel/generator@^7.12.5", "@babel/generator@^7.4.0": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.5.tgz#a2c50de5c8b6d708ab95be5e6053936c1884a4de" - integrity sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A== +"@babel/generator@^7.12.13", "@babel/generator@^7.4.0": + version "7.12.15" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.15.tgz#4617b5d0b25cc572474cc1aafee1edeaf9b5368f" + integrity sha512-6F2xHxBiFXWNSGb7vyCUTBF8RCLY66rS0zEPcP8t/nQyXjha5EuK4z7H5o7fWG8B4M7y6mqVWq1J+1PuwRhecQ== dependencies: - "@babel/types" "^7.12.5" + "@babel/types" "^7.12.13" jsesc "^2.5.1" source-map "^0.5.0" -"@babel/helper-function-name@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" - integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== - dependencies: - "@babel/helper-get-function-arity" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/helper-get-function-arity@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" - integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== - dependencies: - "@babel/types" "^7.10.4" - -"@babel/helper-member-expression-to-functions@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.1.tgz#fba0f2fcff3fba00e6ecb664bb5e6e26e2d6165c" - integrity sha512-k0CIe3tXUKTRSoEx1LQEPFU9vRQfqHtl+kf8eNnDqb4AUJEy5pz6aIiog+YWtVm2jpggjS1laH68bPsR+KWWPQ== - dependencies: - "@babel/types" "^7.12.1" - -"@babel/helper-module-imports@^7.12.1": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz#1bfc0229f794988f76ed0a4d4e90860850b54dfb" - integrity sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA== - dependencies: - "@babel/types" "^7.12.5" - -"@babel/helper-module-transforms@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz#7954fec71f5b32c48e4b303b437c34453fd7247c" - integrity sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w== - dependencies: - "@babel/helper-module-imports" "^7.12.1" - "@babel/helper-replace-supers" "^7.12.1" - "@babel/helper-simple-access" "^7.12.1" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/helper-validator-identifier" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.12.1" - "@babel/types" "^7.12.1" +"@babel/helper-function-name@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a" + integrity sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA== + dependencies: + "@babel/helper-get-function-arity" "^7.12.13" + "@babel/template" "^7.12.13" + "@babel/types" "^7.12.13" + +"@babel/helper-get-function-arity@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583" + integrity sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg== + dependencies: + "@babel/types" "^7.12.13" + +"@babel/helper-member-expression-to-functions@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.13.tgz#c5715695b4f8bab32660dbdcdc2341dec7e3df40" + integrity sha512-B+7nN0gIL8FZ8SvMcF+EPyB21KnCcZHQZFczCxbiNGV/O0rsrSBlWGLzmtBJ3GMjSVMIm4lpFhR+VdVBuIsUcQ== + dependencies: + "@babel/types" "^7.12.13" + +"@babel/helper-module-imports@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz#ec67e4404f41750463e455cc3203f6a32e93fcb0" + integrity sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g== + dependencies: + "@babel/types" "^7.12.13" + +"@babel/helper-module-transforms@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.13.tgz#01afb052dcad2044289b7b20beb3fa8bd0265bea" + integrity sha512-acKF7EjqOR67ASIlDTupwkKM1eUisNAjaSduo5Cz+793ikfnpe7p4Q7B7EWU2PCoSTPWsQkR7hRUWEIZPiVLGA== + dependencies: + "@babel/helper-module-imports" "^7.12.13" + "@babel/helper-replace-supers" "^7.12.13" + "@babel/helper-simple-access" "^7.12.13" + "@babel/helper-split-export-declaration" "^7.12.13" + "@babel/helper-validator-identifier" "^7.12.11" + "@babel/template" "^7.12.13" + "@babel/traverse" "^7.12.13" + "@babel/types" "^7.12.13" lodash "^4.17.19" -"@babel/helper-optimise-call-expression@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz#50dc96413d594f995a77905905b05893cd779673" - integrity sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg== +"@babel/helper-optimise-call-expression@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz#5c02d171b4c8615b1e7163f888c1c81c30a2aaea" + integrity sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA== dependencies: - "@babel/types" "^7.10.4" + "@babel/types" "^7.12.13" -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.8.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" - integrity sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.8.0": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.12.13.tgz#174254d0f2424d8aefb4dd48057511247b0a9eeb" + integrity sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA== -"@babel/helper-replace-supers@^7.12.1": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz#f009a17543bbbbce16b06206ae73b63d3fca68d9" - integrity sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA== +"@babel/helper-replace-supers@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz#00ec4fb6862546bd3d0aff9aac56074277173121" + integrity sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg== dependencies: - "@babel/helper-member-expression-to-functions" "^7.12.1" - "@babel/helper-optimise-call-expression" "^7.10.4" - "@babel/traverse" "^7.12.5" - "@babel/types" "^7.12.5" + "@babel/helper-member-expression-to-functions" "^7.12.13" + "@babel/helper-optimise-call-expression" "^7.12.13" + "@babel/traverse" "^7.12.13" + "@babel/types" "^7.12.13" -"@babel/helper-simple-access@^7.12.1": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz#32427e5aa61547d38eb1e6eaf5fd1426fdad9136" - integrity sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA== +"@babel/helper-simple-access@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz#8478bcc5cacf6aa1672b251c1d2dde5ccd61a6c4" + integrity sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA== dependencies: - "@babel/types" "^7.12.1" + "@babel/types" "^7.12.13" -"@babel/helper-split-export-declaration@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" - integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== +"@babel/helper-split-export-declaration@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz#e9430be00baf3e88b0e13e6f9d4eaf2136372b05" + integrity sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg== dependencies: - "@babel/types" "^7.11.0" + "@babel/types" "^7.12.13" -"@babel/helper-validator-identifier@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" - integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== +"@babel/helper-validator-identifier@^7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" + integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== -"@babel/helpers@^7.12.1": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.5.tgz#1a1ba4a768d9b58310eda516c449913fe647116e" - integrity sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA== +"@babel/helpers@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.13.tgz#3c75e993632e4dadc0274eae219c73eb7645ba47" + integrity sha512-oohVzLRZ3GQEk4Cjhfs9YkJA4TdIDTObdBEZGrd6F/T0GPSnuV6l22eMcxlvcvzVIPH3VTtxbseudM1zIE+rPQ== dependencies: - "@babel/template" "^7.10.4" - "@babel/traverse" "^7.12.5" - "@babel/types" "^7.12.5" + "@babel/template" "^7.12.13" + "@babel/traverse" "^7.12.13" + "@babel/types" "^7.12.13" -"@babel/highlight@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" - integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== +"@babel/highlight@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c" + integrity sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww== dependencies: - "@babel/helper-validator-identifier" "^7.10.4" + "@babel/helper-validator-identifier" "^7.12.11" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.12.3", "@babel/parser@^7.12.5", "@babel/parser@^7.4.3": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.5.tgz#b4af32ddd473c0bfa643bd7ff0728b8e71b81ea0" - integrity sha512-FVM6RZQ0mn2KCf1VUED7KepYeUWoVShczewOCfm3nzoBybaih51h+sYVVGthW9M6lPByEPTQf+xm27PBdlpwmQ== +"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.4.3": + version "7.12.15" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.15.tgz#2b20de7f0b4b332d9b119dd9c33409c538b8aacf" + integrity sha512-AQBOU2Z9kWwSZMd6lNjCX0GUgFonL1wAM1db8L8PMk9UDaGsRCArBkU4Sc+UCM3AE4hjbXx+h58Lb3QT4oRmrA== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -164,11 +163,11 @@ "@babel/helper-plugin-utils" "^7.8.0" "@babel/plugin-syntax-class-properties@^7.8.3": - version "7.12.1" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz#bcb297c5366e79bebadef509549cd93b04f19978" - integrity sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA== + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== dependencies: - "@babel/helper-plugin-utils" "^7.10.4" + "@babel/helper-plugin-utils" "^7.12.13" "@babel/plugin-syntax-import-meta@^7.8.3": version "7.10.4" @@ -227,51 +226,42 @@ "@babel/helper-plugin-utils" "^7.8.0" "@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/template@^7.10.4", "@babel/template@^7.3.3", "@babel/template@^7.4.0": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" - integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.10.4" - "@babel/types" "^7.10.4" - -"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.5", "@babel/traverse@^7.4.3": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.5.tgz#78a0c68c8e8a35e4cacfd31db8bb303d5606f095" - integrity sha512-xa15FbQnias7z9a62LwYAA5SZZPkHIXpd42C6uW68o8uTuua96FHZy1y61Va5P/i83FAAcMpW8+A/QayntzuqA== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.12.5" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.12.5" - "@babel/types" "^7.12.5" + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.13.tgz#c5f0fa6e249f5b739727f923540cf7a806130178" + integrity sha512-A81F9pDwyS7yM//KwbCSDqy3Uj4NMIurtplxphWxoYtNPov7cJsDkAFNNyVlIZ3jwGycVsurZ+LtOA8gZ376iQ== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/template@^7.12.13", "@babel/template@^7.3.3", "@babel/template@^7.4.0": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" + integrity sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA== + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/parser" "^7.12.13" + "@babel/types" "^7.12.13" + +"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.13", "@babel/traverse@^7.4.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.13.tgz#689f0e4b4c08587ad26622832632735fb8c4e0c0" + integrity sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA== + dependencies: + "@babel/code-frame" "^7.12.13" + "@babel/generator" "^7.12.13" + "@babel/helper-function-name" "^7.12.13" + "@babel/helper-split-export-declaration" "^7.12.13" + "@babel/parser" "^7.12.13" + "@babel/types" "^7.12.13" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.19" -"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.11.0", "@babel/types@^7.12.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.0": - version "7.12.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.6.tgz#ae0e55ef1cce1fbc881cd26f8234eb3e657edc96" - integrity sha512-hwyjw6GvjBLiyy3W0YQf0Z5Zf4NpYejUnKFcfcUhZCSffoBBp30w6wP2Wn6pk31jMYZvcOrB/1b7cGXvEoKogA== +"@babel/types@^7.0.0", "@babel/types@^7.12.13", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.0": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.13.tgz#8be1aa8f2c876da11a9cf650c0ecf656913ad611" + integrity sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ== dependencies: - "@babel/helper-validator-identifier" "^7.10.4" - lodash "^4.17.19" - to-fast-properties "^2.0.0" - -"@babel/types@^7.12.1": - version "7.12.7" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.7.tgz#6039ff1e242640a29452c9ae572162ec9a8f5d13" - integrity sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ== - dependencies: - "@babel/helper-validator-identifier" "^7.10.4" + "@babel/helper-validator-identifier" "^7.12.11" lodash "^4.17.19" to-fast-properties "^2.0.0" @@ -293,10 +283,10 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@eslint/eslintrc@^0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.2.1.tgz#f72069c330461a06684d119384435e12a5d76e3c" - integrity sha512-XRUeBZ5zBWLYgSANMpThFddrZZkEbGHgUdt5UJjZfnlN9BGCiUBrf+nvbRupSjMvqzwnQN0qwCmOxITt1cfywA== +"@eslint/eslintrc@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.3.0.tgz#d736d6963d7003b6514e6324bec9c602ac340318" + integrity sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg== dependencies: ajv "^6.12.4" debug "^4.1.1" @@ -305,7 +295,7 @@ ignore "^4.0.6" import-fresh "^3.2.1" js-yaml "^3.13.1" - lodash "^4.17.19" + lodash "^4.17.20" minimatch "^3.0.4" strip-json-comments "^3.1.1" @@ -570,10 +560,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@jsii/spec@^1.15.0": - version "1.15.0" - resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.15.0.tgz#e7f3a18d231ef6d1826ce7257de7b2f6e2071f40" - integrity sha512-aybTXziVMQcCp2EGIMCJgAs4uvXtN/iBVeJMyBRzqznhtctG4flOu37FjIdib/OOJLrRLt4NAA95R5kNm/jLpA== +"@jsii/spec@^1.20.1": + version "1.20.1" + resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.20.1.tgz#931a4e8280ab2f2314452a92fb91f0b40d5e8f77" + integrity sha512-+ot9SPb/lFnRiPlhWUGLMp8owpr/pnpFX0/iIUhZ0kJ1LIAUde7TSuaz70mV6x+ES2wvVXKsgfu2WTMFHIAwZQ== dependencies: jsonschema "^1.4.0" @@ -1270,18 +1260,18 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" -"@nodelib/fs.scandir@2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" - integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== +"@nodelib/fs.scandir@2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69" + integrity sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA== dependencies: - "@nodelib/fs.stat" "2.0.3" + "@nodelib/fs.stat" "2.0.4" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" - integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== +"@nodelib/fs.stat@2.0.4", "@nodelib/fs.stat@^2.0.2": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655" + integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q== "@nodelib/fs.stat@^1.1.2": version "1.1.3" @@ -1289,50 +1279,55 @@ integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== "@nodelib/fs.walk@^1.2.3": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" - integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + version "1.2.6" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz#cce9396b30aa5afe9e3756608f5831adcb53d063" + integrity sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== dependencies: - "@nodelib/fs.scandir" "2.1.3" + "@nodelib/fs.scandir" "2.1.4" fastq "^1.6.0" -"@octokit/auth-token@^2.4.0": - version "2.4.3" - resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.3.tgz#b868b5f2366533a7e62933eaa1181a8924228cc4" - integrity sha512-fdGoOQ3kQJh+hrilc0Plg50xSfaCKOeYN9t6dpJKXN9BxhhfquL0OzoQXg3spLYymL5rm29uPeI3KEXRaZQ9zg== +"@octokit/auth-token@^2.4.0", "@octokit/auth-token@^2.4.4": + version "2.4.5" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.5.tgz#568ccfb8cb46f36441fac094ce34f7a875b197f3" + integrity sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA== dependencies: - "@octokit/types" "^5.0.0" + "@octokit/types" "^6.0.3" -"@octokit/core@^3.0.0": - version "3.2.1" - resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.2.1.tgz#9e04df3f4e7f825ac0559327490ce34299140af5" - integrity sha512-XfFSDDwv6tclUenS0EmB6iA7u+4aOHBT1Lz4PtQNQQg3hBbNaR/+Uv5URU+egeIuuGAiMRiDyY92G4GBOWOqDA== +"@octokit/core@^3.2.3": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.2.5.tgz#57becbd5fd789b0592b915840855f3a5f233d554" + integrity sha512-+DCtPykGnvXKWWQI0E1XD+CCeWSBhB6kwItXqfFmNBlIlhczuDPbg+P6BtLnVBaRJDAjv+1mrUJuRsFSjktopg== dependencies: - "@octokit/auth-token" "^2.4.0" - "@octokit/graphql" "^4.3.1" - "@octokit/request" "^5.4.0" - "@octokit/types" "^5.0.0" + "@octokit/auth-token" "^2.4.4" + "@octokit/graphql" "^4.5.8" + "@octokit/request" "^5.4.12" + "@octokit/types" "^6.0.3" before-after-hook "^2.1.0" universal-user-agent "^6.0.0" "@octokit/endpoint@^6.0.1": - version "6.0.9" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.9.tgz#c6a772e024202b1bd19ab69f90e0536a2598b13e" - integrity sha512-3VPLbcCuqji4IFTclNUtGdp9v7g+nspWdiCUbK3+iPMjJCZ6LEhn1ts626bWLOn0GiDb6j+uqGvPpqLnY7pBgw== + version "6.0.11" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.11.tgz#082adc2aebca6dcefa1fb383f5efb3ed081949d1" + integrity sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ== dependencies: - "@octokit/types" "^5.0.0" + "@octokit/types" "^6.0.3" is-plain-object "^5.0.0" universal-user-agent "^6.0.0" -"@octokit/graphql@^4.3.1": - version "4.5.7" - resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.5.7.tgz#f4562dcd9e80ea94602068e85aefac19a88f8578" - integrity sha512-Gk0AR+DcwIK/lK/GX+OQ99UqtenQhcbrhHHfOYlrCQe17ADnX3EKAOKRsAZ9qZvpi5MuwWm/Nm+9aO2kTDSdyA== +"@octokit/graphql@^4.5.8": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.6.0.tgz#f9abca55f82183964a33439d5264674c701c3327" + integrity sha512-CJ6n7izLFXLvPZaWzCQDjU/RP+vHiZmWdOunaCS87v+2jxMsW9FB5ktfIxybRBxZjxuJGRnxk7xJecWTVxFUYQ== dependencies: "@octokit/request" "^5.3.0" - "@octokit/types" "^5.0.0" + "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" +"@octokit/openapi-types@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-4.0.1.tgz#bafd3d173974827ba0b733fcca7f1860cb71a9aa" + integrity sha512-k2hRcfcLRyPJjtYfJLzg404n7HZ6sUpAWAR/uNI8tf96NgatWOpw1ocdF+WFfx/trO1ivBh7ckynO1rn+xAw/Q== + "@octokit/plugin-enterprise-rest@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" @@ -1345,17 +1340,17 @@ dependencies: "@octokit/types" "^2.0.1" -"@octokit/plugin-paginate-rest@^2.2.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.6.0.tgz#03416396e7a227b268c5b827365238f620a9c5c1" - integrity sha512-o+O8c1PqsC5++BHXfMZabRRsBIVb34tXPWyQLyp2IXq5MmkxdipS7TXM4Y9ldL1PzY9CTrCsn/lzFFJGM3oRRA== +"@octokit/plugin-paginate-rest@^2.6.2": + version "2.9.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.9.1.tgz#e9bb34a89b7ed5b801f1c976feeb9b0078ecd201" + integrity sha512-8wnuWGjwDIEobbBet2xAjZwgiMVTgIer5wBsnGXzV3lJ4yqphLU2FEMpkhSrDx7y+WkZDfZ+V+1cFMZ1mAaFag== dependencies: - "@octokit/types" "^5.5.0" + "@octokit/types" "^6.8.0" -"@octokit/plugin-request-log@^1.0.0": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.2.tgz#394d59ec734cd2f122431fbaf05099861ece3c44" - integrity sha512-oTJSNAmBqyDR41uSMunLQKMX0jmEXbwD1fpz8FG27lScV3RhtGfBa1/BBLym+PxcC16IBlF7KH9vP1BUYxA+Eg== +"@octokit/plugin-request-log@^1.0.0", "@octokit/plugin-request-log@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.3.tgz#70a62be213e1edc04bb8897ee48c311482f9700d" + integrity sha512-4RFU4li238jMJAzLgAwkBAw+4Loile5haQMQr+uhFq27BmyJXcXSKvoQKqh0agsZEiUlW6iSv3FAgvmGkur7OQ== "@octokit/plugin-rest-endpoint-methods@2.4.0": version "2.4.0" @@ -1365,12 +1360,12 @@ "@octokit/types" "^2.0.1" deprecation "^2.3.1" -"@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== +"@octokit/plugin-rest-endpoint-methods@4.10.1": + version "4.10.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.10.1.tgz#b7a9181d1f52fef70a13945c5b49cffa51862da1" + integrity sha512-YGMiEidTORzgUmYZu0eH4q2k8kgQSHQMuBOBYiKxUYs/nXea4q/Ze6tDzjcRAPmHNJYXrENs1bEMlcdGKT+8ug== dependencies: - "@octokit/types" "^5.5.0" + "@octokit/types" "^6.8.2" deprecation "^2.3.1" "@octokit/request-error@^1.0.2": @@ -1383,22 +1378,22 @@ once "^1.4.0" "@octokit/request-error@^2.0.0": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.3.tgz#b51b200052bf483f6fa56c9e7e3aa51ead36ecd8" - integrity sha512-GgD5z8Btm301i2zfvJLk/mkhvGCdjQ7wT8xF9ov5noQY8WbKZDH9cOBqXzoeKd1mLr1xH2FwbtGso135zGBgTA== + version "2.0.5" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.5.tgz#72cc91edc870281ad583a42619256b380c600143" + integrity sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg== dependencies: - "@octokit/types" "^5.0.1" + "@octokit/types" "^6.0.3" deprecation "^2.0.0" once "^1.4.0" -"@octokit/request@^5.2.0", "@octokit/request@^5.3.0", "@octokit/request@^5.4.0": - version "5.4.10" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.10.tgz#402d2c53768bde12b99348329ba4129746aebb9c" - integrity sha512-egA49HkqEORVGDZGav1mh+VD+7uLgOxtn5oODj6guJk0HCy+YBSYapFkSLFgeYj3Fr18ZULKGURkjyhkAChylw== +"@octokit/request@^5.2.0", "@octokit/request@^5.3.0", "@octokit/request@^5.4.12": + version "5.4.14" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.14.tgz#ec5f96f78333bb2af390afa5ff66f114b063bc96" + integrity sha512-VkmtacOIQp9daSnBmDI92xNIeLuSRDOIuplp/CJomkvzt7M18NXgG044Cx/LFKLgjKt9T2tZR6AtJayba9GTSA== dependencies: "@octokit/endpoint" "^6.0.1" "@octokit/request-error" "^2.0.0" - "@octokit/types" "^5.0.0" + "@octokit/types" "^6.7.1" deprecation "^2.0.0" is-plain-object "^5.0.0" node-fetch "^2.6.1" @@ -1427,15 +1422,15 @@ once "^1.4.0" universal-user-agent "^4.0.0" -"@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== +"@octokit/rest@^18.1.0": + version "18.1.0" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.1.0.tgz#9bf72604911a3433165bcc924263c9a706d32804" + integrity sha512-YQfpTzWV3jdzDPyXQVO54f5I2t1zxk/S53Vbe+Aa5vQj6MdTx6sNEWzmUzUO8lSVowbGOnjcQHzW1A8ATr+/7g== 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.1" + "@octokit/core" "^3.2.3" + "@octokit/plugin-paginate-rest" "^2.6.2" + "@octokit/plugin-request-log" "^1.0.2" + "@octokit/plugin-rest-endpoint-methods" "4.10.1" "@octokit/types@^2.0.0", "@octokit/types@^2.0.1": version "2.16.2" @@ -1444,17 +1439,18 @@ dependencies: "@types/node" ">= 8" -"@octokit/types@^5.0.0", "@octokit/types@^5.0.1", "@octokit/types@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-5.5.0.tgz#e5f06e8db21246ca102aa28444cdb13ae17a139b" - integrity sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ== +"@octokit/types@^6.0.3", "@octokit/types@^6.7.1", "@octokit/types@^6.8.0", "@octokit/types@^6.8.2": + version "6.8.2" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.8.2.tgz#ce4872e038d6df38b2d3c21bc12329af0b10facb" + integrity sha512-RpG0NJd7OKSkWptiFhy1xCLkThs5YoDIKM21lEtDmUvSpbaIEfrxzckWLUGDFfF8RydSyngo44gDv8m2hHruUg== dependencies: + "@octokit/openapi-types" "^4.0.0" "@types/node" ">= 8" -"@sinonjs/commons@^1", "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.1": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217" - integrity sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw== +"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.1": + version "1.8.2" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.2.tgz#858f5c4b48d80778fde4b9d541f27edc0d56488b" + integrity sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw== dependencies: type-detect "4.0.8" @@ -1465,18 +1461,10 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@sinonjs/formatio@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-5.0.1.tgz#f13e713cb3313b1ab965901b01b0828ea6b77089" - integrity sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ== - dependencies: - "@sinonjs/commons" "^1" - "@sinonjs/samsam" "^5.0.2" - -"@sinonjs/samsam@^5.0.2", "@sinonjs/samsam@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.2.0.tgz#fcff83ab86f83b5498f4a967869c079408d9b5eb" - integrity sha512-CaIcyX5cDsjcW/ab7HposFWzV1kC++4HNsfnEdFJa7cP1QIuILAKV+BgfeqRXhcnSAc76r/Rh/O5C+300BwUIw== +"@sinonjs/samsam@^5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.3.1.tgz#375a45fe6ed4e92fca2fb920e007c48232a6507f" + integrity sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg== dependencies: "@sinonjs/commons" "^1.6.0" lodash.get "^4.4.2" @@ -1499,10 +1487,10 @@ dependencies: "@types/glob" "*" -"@types/aws-lambda@^8.10.64": - version "8.10.64" - resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.64.tgz#4bdcb725aef96bef0cb1decf19c7efff1df22fe7" - integrity sha512-LRKk2UQCSi7BsO5TlfSI8cTNpOGz+MH6+RXEWtuZmxJficQgxwEYJDiKVirzgyiHce0L0F4CqCVvKTwblAeOUw== +"@types/aws-lambda@^8.10.71": + version "8.10.71" + resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.71.tgz#ab3084038411ce42f63b975e67aafb163f3aa353" + integrity sha512-l0Lag6qq06AlKllprAJ3pbgVUbXCjRGRb7VpHow8IMn2BMHTPR0t5OD97/w8CR1+wA5XZuWQoXLjYvdlk2kQrQ== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.12" @@ -1531,9 +1519,9 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6": - version "7.0.15" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.15.tgz#db9e4238931eb69ef8aab0ad6523d4d4caa39d03" - integrity sha512-Pzh9O3sTK8V6I1olsXpCfj2k/ygO2q1X0vhhnDrEQyYLHZesWz+zMZMVcwXLCYf0U36EtmyYaFGPfXlTtDHe3A== + version "7.11.0" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.11.0.tgz#b9a1efa635201ba9bc850323a8793ee2d36c04a0" + integrity sha512-kSjgDMZONiIfSH1Nxcr5JIRMwUetDki63FSQfpTCz8ogF3Ulqm8+mr5f78dUYs6vMiB6gBusQqfQmBvHZj/lwg== dependencies: "@babel/types" "^7.3.0" @@ -1546,9 +1534,9 @@ "@types/json-schema" "*" "@types/estree@*": - version "0.0.45" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884" - integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g== + version "0.0.46" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.46.tgz#0fb6bfbbeabd7a30880504993369c4bf1deab1fe" + integrity sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg== "@types/fs-extra@^8.1.1": version "8.1.1" @@ -1591,18 +1579,18 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@26.x", "@types/jest@^26.0.15": - version "26.0.15" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.15.tgz#12e02c0372ad0548e07b9f4e19132b834cb1effe" - integrity sha512-s2VMReFXRg9XXxV+CW9e5Nz8fH2K1aEhwgjUqPPbQd7g95T0laAcvLv032EhFHIa5GHsZ8W7iJEQVaJq6k3Gog== +"@types/jest@26.x", "@types/jest@^26.0.20": + version "26.0.20" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.20.tgz#cd2f2702ecf69e86b586e1f5223a60e454056307" + integrity sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA== dependencies: jest-diff "^26.0.0" pretty-format "^26.0.0" "@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.6": - version "7.0.6" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" - integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" + integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== "@types/json5@^0.0.29": version "0.0.29" @@ -1651,9 +1639,9 @@ integrity sha1-m6It838H43gP/4Ux0aOOYz+UV6U= "@types/node@*", "@types/node@>= 8": - version "14.14.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d" - integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg== + version "14.14.25" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.25.tgz#15967a7b577ff81383f9b888aa6705d43fbbae93" + integrity sha512-EPpXLOVqDvisVxtlbvzfyqSsFeQxltFbluZNRndIb8tr9KiBnYNLzrc1N3pyKUCww2RNrfHDViqDWWE1LCJQtQ== "@types/node@^10.17.51": version "10.17.51" @@ -1671,9 +1659,9 @@ integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== "@types/prettier@^2.0.0": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.5.tgz#b6ab3bba29e16b821d84e09ecfaded462b816b00" - integrity sha512-UEyp8LwZ4Dg30kVU2Q3amHHyTn1jEdhCIE59ANed76GaT1Vp76DD3ZWSAxgCrw6wJ0TqeoBpqmfUHiUDPs//HQ== + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.0.tgz#a4e8205a4955690eef712a6d0394a1d2e121e721" + integrity sha512-O3SQC6+6AySHwrspYn2UvC6tjo6jCTMMmylxZUFhE1CulVu5l3AxU6ca9lrJDTQDVllF62LIxVSx5fuYL6LiZg== "@types/promptly@^3.0.1": version "3.0.1" @@ -1697,10 +1685,10 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb" integrity sha512-+nVsLKlcUCeMzD2ufHEYuJ9a2ovstb6Dp52A5VsoKxDXgvE051XgHI/33I1EymwkRGQkwnA0LkhnUzituGs4EQ== -"@types/sinon@^9.0.9": - version "9.0.9" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.9.tgz#115843b491583f924080f684b6d0d7438344f73c" - integrity sha512-z/y8maYOQyYLyqaOB+dYQ6i0pxKLOsfwCmHmn4T7jS/SDHicIslr37oE3Dg8SCqKrKeBy6Lemu7do2yy+unLrw== +"@types/sinon@^9.0.10": + version "9.0.10" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.10.tgz#7fb9bcb6794262482859cab66d59132fca18fcf7" + integrity sha512-/faDC0erR06wMdybwI/uR8wEKV/E83T0k4sepIpB7gXuy2gzx2xiOjmztq6a2Y6rIGJ04D+6UU0VBmWy+4HEMA== dependencies: "@types/sinonjs__fake-timers" "*" @@ -1751,21 +1739,14 @@ yaml "*" "@types/yargs-parser@*": - version "15.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" - integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== + version "20.2.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9" + integrity sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA== -"@types/yargs@^15.0.0": - version "15.0.9" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.9.tgz#524cd7998fe810cdb02f26101b699cccd156ff19" - integrity sha512-HmU8SeIRhZCWcnRskCs36Q1Q00KBV6Cqh/ora8WN1+22dY07AZdn6Gel8QZ3t26XYPImtcL8WV/eqjhVmMEw4g== - dependencies: - "@types/yargs-parser" "*" - -"@types/yargs@^15.0.10": - version "15.0.10" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.10.tgz#0fe3c8173a0d5c3e780b389050140c3f5ea6ea74" - integrity sha512-z8PNtlhrj7eJNLmrAivM7rjBESG6JwC5xP3RVk12i/8HVP7Xnx/sEmERnRImyEuUaJfO942X0qMOYsoupaJbZQ== +"@types/yargs@^15.0.0", "@types/yargs@^15.0.13": + version "15.0.13" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.13.tgz#34f7fec8b389d7f3c1fd08026a5763e072d3c6dc" + integrity sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ== dependencies: "@types/yargs-parser" "*" @@ -1774,13 +1755,13 @@ resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.4.tgz#445251eb00bd9c1e751f82c7c6bf4f714edfd464" integrity sha512-/emrKCfQMQmFCqRqqBJ0JueHBT06jBRM3e8OgnvDUcvuExONujIk2hFA5dNsN9Nt41ljGVDdChvCydATZ+KOZw== -"@typescript-eslint/eslint-plugin@^4.14.0": - version "4.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.14.0.tgz#92db8e7c357ed7d69632d6843ca70b71be3a721d" - integrity sha512-IJ5e2W7uFNfg4qh9eHkHRUCbgZ8VKtGwD07kannJvM5t/GU8P8+24NX8gi3Hf5jST5oWPY8kyV1s/WtfiZ4+Ww== +"@typescript-eslint/eslint-plugin@^4.15.0": + version "4.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.15.0.tgz#13a5a07cf30d0d5781e43480aa2a8d38d308b084" + integrity sha512-DJgdGZW+8CFUTz5C/dnn4ONcUm2h2T0itWD85Ob5/V27Ndie8hUoX5HKyGssvR8sUMkAIlUc/AMK67Lqa3kBIQ== dependencies: - "@typescript-eslint/experimental-utils" "4.14.0" - "@typescript-eslint/scope-manager" "4.14.0" + "@typescript-eslint/experimental-utils" "4.15.0" + "@typescript-eslint/scope-manager" "4.15.0" debug "^4.1.1" functional-red-black-tree "^1.0.1" lodash "^4.17.15" @@ -1788,61 +1769,61 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@4.14.0", "@typescript-eslint/experimental-utils@^4.0.1": - version "4.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.14.0.tgz#5aa7b006736634f588a69ee343ca959cd09988df" - integrity sha512-6i6eAoiPlXMKRbXzvoQD5Yn9L7k9ezzGRvzC/x1V3650rUk3c3AOjQyGYyF9BDxQQDK2ElmKOZRD0CbtdkMzQQ== +"@typescript-eslint/experimental-utils@4.15.0", "@typescript-eslint/experimental-utils@^4.0.1": + version "4.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.15.0.tgz#b87c36410a9b23f637689427be85007a2ec1a9c6" + integrity sha512-V4vaDWvxA2zgesg4KPgEGiomWEBpJXvY4ZX34Y3qxK8LUm5I87L+qGIOTd9tHZOARXNRt9pLbblSKiYBlGMawg== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.14.0" - "@typescript-eslint/types" "4.14.0" - "@typescript-eslint/typescript-estree" "4.14.0" + "@typescript-eslint/scope-manager" "4.15.0" + "@typescript-eslint/types" "4.15.0" + "@typescript-eslint/typescript-estree" "4.15.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^4.7.0": - version "4.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.7.0.tgz#44bdab0f788b478178368baa65d3365fdc63da1c" - integrity sha512-+meGV8bMP1sJHBI2AFq1GeTwofcGiur8LoIr6v+rEmD9knyCqDlrQcFHR0KDDfldHIFDU/enZ53fla6ReF4wRw== +"@typescript-eslint/parser@^4.14.2": + version "4.14.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.14.2.tgz#31e216e4baab678a56e539f9db9862e2542c98d0" + integrity sha512-ipqSP6EuUsMu3E10EZIApOJgWSpcNXeKZaFeNKQyzqxnQl8eQCbV+TSNsl+s2GViX2d18m1rq3CWgnpOxDPgHg== dependencies: - "@typescript-eslint/scope-manager" "4.7.0" - "@typescript-eslint/types" "4.7.0" - "@typescript-eslint/typescript-estree" "4.7.0" + "@typescript-eslint/scope-manager" "4.14.2" + "@typescript-eslint/types" "4.14.2" + "@typescript-eslint/typescript-estree" "4.14.2" debug "^4.1.1" -"@typescript-eslint/scope-manager@4.14.0": - version "4.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.14.0.tgz#55a4743095d684e1f7b7180c4bac2a0a3727f517" - integrity sha512-/J+LlRMdbPh4RdL4hfP1eCwHN5bAhFAGOTsvE6SxsrM/47XQiPSgF5MDgLyp/i9kbZV9Lx80DW0OpPkzL+uf8Q== +"@typescript-eslint/scope-manager@4.14.2": + version "4.14.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.14.2.tgz#64cbc9ca64b60069aae0c060b2bf81163243b266" + integrity sha512-cuV9wMrzKm6yIuV48aTPfIeqErt5xceTheAgk70N1V4/2Ecj+fhl34iro/vIssJlb7XtzcaD07hWk7Jk0nKghg== dependencies: - "@typescript-eslint/types" "4.14.0" - "@typescript-eslint/visitor-keys" "4.14.0" + "@typescript-eslint/types" "4.14.2" + "@typescript-eslint/visitor-keys" "4.14.2" -"@typescript-eslint/scope-manager@4.7.0": - version "4.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.7.0.tgz#2115526085fb72723ccdc1eeae75dec7126220ed" - integrity sha512-ILITvqwDJYbcDCROj6+Ob0oCKNg3SH46iWcNcTIT9B5aiVssoTYkhKjxOMNzR1F7WSJkik4zmuqve5MdnA0DyA== +"@typescript-eslint/scope-manager@4.15.0": + version "4.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.15.0.tgz#c42703558ea6daaaba51a9c3a86f2902dbab9432" + integrity sha512-CSNBZnCC2jEA/a+pR9Ljh8Y+5TY5qgbPz7ICEk9WCpSEgT6Pi7H2RIjxfrrbUXvotd6ta+i27sssKEH8Azm75g== dependencies: - "@typescript-eslint/types" "4.7.0" - "@typescript-eslint/visitor-keys" "4.7.0" + "@typescript-eslint/types" "4.15.0" + "@typescript-eslint/visitor-keys" "4.15.0" -"@typescript-eslint/types@4.14.0": - version "4.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.0.tgz#d8a8202d9b58831d6fd9cee2ba12f8a5a5dd44b6" - integrity sha512-VsQE4VvpldHrTFuVPY1ZnHn/Txw6cZGjL48e+iBxTi2ksa9DmebKjAeFmTVAYoSkTk7gjA7UqJ7pIsyifTsI4A== +"@typescript-eslint/types@4.14.2": + version "4.14.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.2.tgz#d96da62be22dc9dc6a06647f3633815350fb3174" + integrity sha512-LltxawRW6wXy4Gck6ZKlBD05tCHQUj4KLn4iR69IyRiDHX3d3NCAhO+ix5OR2Q+q9bjCrHE/HKt+riZkd1At8Q== -"@typescript-eslint/types@4.7.0": - version "4.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.7.0.tgz#5e95ef5c740f43d942542b35811f87b62fccca69" - integrity sha512-uLszFe0wExJc+I7q0Z/+BnP7wao/kzX0hB5vJn4LIgrfrMLgnB2UXoReV19lkJQS1a1mHWGGODSxnBx6JQC3Sg== +"@typescript-eslint/types@4.15.0": + version "4.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.15.0.tgz#3011ae1ac3299bb9a5ac56bdd297cccf679d3662" + integrity sha512-su4RHkJhS+iFwyqyXHcS8EGPlUVoC+XREfy5daivjLur9JP8GhvTmDipuRpcujtGC4M+GYhUOJCPDE3rC5NJrg== -"@typescript-eslint/typescript-estree@4.14.0": - version "4.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.0.tgz#4bcd67486e9acafc3d0c982b23a9ab8ac8911ed7" - integrity sha512-wRjZ5qLao+bvS2F7pX4qi2oLcOONIB+ru8RGBieDptq/SudYwshveORwCVU4/yMAd4GK7Fsf8Uq1tjV838erag== +"@typescript-eslint/typescript-estree@4.14.2": + version "4.14.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.14.2.tgz#9c5ebd8cae4d7b014f890acd81e8e17f309c9df9" + integrity sha512-ESiFl8afXxt1dNj8ENEZT12p+jl9PqRur+Y19m0Z/SPikGL6rqq4e7Me60SU9a2M28uz48/8yct97VQYaGl0Vg== dependencies: - "@typescript-eslint/types" "4.14.0" - "@typescript-eslint/visitor-keys" "4.14.0" + "@typescript-eslint/types" "4.14.2" + "@typescript-eslint/visitor-keys" "4.14.2" debug "^4.1.1" globby "^11.0.1" is-glob "^4.0.1" @@ -1850,34 +1831,33 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@4.7.0": - version "4.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.7.0.tgz#539531167f05ba20eb0b6785567076679e29d393" - integrity sha512-5XZRQznD1MfUmxu1t8/j2Af4OxbA7EFU2rbo0No7meb46eHgGkSieFdfV6omiC/DGIBhH9H9gXn7okBbVOm8jw== +"@typescript-eslint/typescript-estree@4.15.0": + version "4.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.15.0.tgz#402c86a7d2111c1f7a2513022f22a38a395b7f93" + integrity sha512-jG6xTmcNbi6xzZq0SdWh7wQ9cMb2pqXaUp6bUZOMsIlu5aOlxGxgE/t6L/gPybybQGvdguajXGkZKSndZJpksA== dependencies: - "@typescript-eslint/types" "4.7.0" - "@typescript-eslint/visitor-keys" "4.7.0" + "@typescript-eslint/types" "4.15.0" + "@typescript-eslint/visitor-keys" "4.15.0" debug "^4.1.1" globby "^11.0.1" is-glob "^4.0.1" - lodash "^4.17.15" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@4.14.0": - version "4.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.0.tgz#b1090d9d2955b044b2ea2904a22496849acbdf54" - integrity sha512-MeHHzUyRI50DuiPgV9+LxcM52FCJFYjJiWHtXlbyC27b80mfOwKeiKI+MHOTEpcpfmoPFm/vvQS88bYIx6PZTA== +"@typescript-eslint/visitor-keys@4.14.2": + version "4.14.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.2.tgz#997cbe2cb0690e1f384a833f64794e98727c70c6" + integrity sha512-KBB+xLBxnBdTENs/rUgeUKO0UkPBRs2vD09oMRRIkj5BEN8PX1ToXV532desXfpQnZsYTyLLviS7JrPhdL154w== dependencies: - "@typescript-eslint/types" "4.14.0" + "@typescript-eslint/types" "4.14.2" eslint-visitor-keys "^2.0.0" -"@typescript-eslint/visitor-keys@4.7.0": - version "4.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.7.0.tgz#6783824f22acfc49e754970ed21b88ac03b80e6f" - integrity sha512-aDJDWuCRsf1lXOtignlfiPODkzSxxop7D0rZ91L6ZuMlcMCSh0YyK+gAfo5zN/ih6WxMwhoXgJWC3cWQdaKC+A== +"@typescript-eslint/visitor-keys@4.15.0": + version "4.15.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.0.tgz#2a07768df30c8a5673f1bce406338a07fdec38ca" + integrity sha512-RnDtJwOwFucWFAMjG3ghCG/ikImFJFEg20DI7mn4pHEx3vC48lIAoyjhffvfHmErRDboUPC7p9Z2il4CLb7qxA== dependencies: - "@typescript-eslint/types" "4.7.0" + "@typescript-eslint/types" "4.15.0" eslint-visitor-keys "^2.0.0" "@yarnpkg/lockfile@^1.1.0": @@ -1920,7 +1900,7 @@ acorn-globals@^6.0.0: acorn "^7.1.1" acorn-walk "^7.1.1" -acorn-jsx@^5.2.0: +acorn-jsx@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== @@ -1976,7 +1956,7 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: +ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1987,9 +1967,9 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4: uri-js "^4.2.2" ajv@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.0.3.tgz#13ae747eff125cafb230ac504b2406cf371eece2" - integrity sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ== + version "7.0.4" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.0.4.tgz#827e5f5ae32f5e5c1637db61f253a112229b5e2f" + integrity sha512-xzzzaqgEQfmuhbhAoqjJ8T/1okb6gAzXn/eQRNpAN1AEUoHJTNF9xCDRTtf/s3SKldtZfa+RJeTs+BQq+eZ/sw== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -2068,6 +2048,11 @@ 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" @@ -2187,12 +2172,14 @@ array-ify@^1.0.0: integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= array-includes@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" - integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ== + version "3.1.2" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.2.tgz#a8db03e0b88c8c6aeddc49cb132f9bcab4ebf9c8" + integrity sha512-w2GspexNQpx+PutG3QpT437/BenZBj0M/MZGn5mzv/MofYqo0xmRHzn4lFsoDlWJ+THYsGJmFlW68WlDFx7VRw== dependencies: + call-bind "^1.0.0" define-properties "^1.1.3" - es-abstract "^1.17.0" + es-abstract "^1.18.0-next.1" + get-intrinsic "^1.0.1" is-string "^1.0.5" array-union@^1.0.2: @@ -2218,12 +2205,13 @@ array-unique@^0.3.2: integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= array.prototype.flat@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b" - integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123" + integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg== dependencies: + call-bind "^1.0.0" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" + es-abstract "^1.18.0-next.1" arrify@^1.0.1: version "1.0.1" @@ -2259,11 +2247,6 @@ ast-types@^0.13.2: dependencies: tslib "^2.0.1" -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== - astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" @@ -2294,7 +2277,7 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: +available-typed-arrays@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ== @@ -2310,10 +2293,10 @@ aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" -aws-sdk@^2.637.0, aws-sdk@^2.830.0: - version "2.830.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.830.0.tgz#1d3631d573d18c48373046da7ad92855a7fd1636" - integrity sha512-vFatoWkdJmRzpymWbqsuwVsAJdhdAvU2JcM9jKRENTNKJw90ljnLyeP1eKCp4O3/4Lg43PVBwY/KUqPy4wL+OA== +aws-sdk@^2.596.0, aws-sdk@^2.637.0, aws-sdk@^2.842.0: + version "2.842.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.842.0.tgz#2673f72c0981c4697f8ed5dc39061cff98d9057f" + integrity sha512-9vjVDxsLzNI79JChUgEHDUpv2obkTe35F3oGFGViKsf4C7xlOexzKOCfTRNcgzh0MON6rVDFpYBtF2LlEyDGKg== dependencies: buffer "4.9.2" events "1.1.1" @@ -2335,12 +2318,12 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== -axios@^0.19.0: - version "0.19.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" - integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== +axios@^0.21.1: + version "0.21.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" + integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== dependencies: - follow-redirects "1.5.10" + follow-redirects "^1.10.0" babel-jest@^26.6.3: version "26.6.3" @@ -2378,9 +2361,9 @@ babel-plugin-jest-hoist@^26.6.2: "@types/babel__traverse" "^7.0.6" 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== + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-bigint" "^7.8.3" @@ -2434,9 +2417,9 @@ bcrypt-pbkdf@^1.0.0: tweetnacl "^0.14.3" before-after-hook@^2.0.0, before-after-hook@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" - integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== + version "2.1.1" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.1.tgz#99ae36992b5cfab4a83f6bee74ab27835f28f405" + integrity sha512-5ekuQOvO04MDj7kYZJaMab2S8SPjGJbotVNyv7QYFCOAwrGZs/YnoDNlh1U+m5hl7H2D/+n0taaAV/tfyd3KMA== bind-obj-methods@^2.0.0: version "2.0.0" @@ -2444,9 +2427,9 @@ bind-obj-methods@^2.0.0: integrity sha512-3/qRXczDi2Cdbz6jE+W3IflJOutRVica8frpBn14de1mBOkzDo+6tY33kNhvkw54Kn3PzRRD2VnGbGPcTAk4sw== bl@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" - integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== + version "4.0.4" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.4.tgz#f4fda39f81a811d0df6368c1ed91dae499d1c900" + integrity sha512-7tdr4EpSd7jJ6tuQ21vu2ke8w7pNEstzj1O8wwq6sNNzO3UDi5MA8Gny/gquCj7r2C6fHudg8tKRGyjRgmvNxQ== dependencies: buffer "^5.5.0" inherits "^2.0.4" @@ -2615,13 +2598,13 @@ caching-transform@^4.0.0: package-hash "^4.0.0" write-file-atomic "^3.0.0" -call-bind@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.0.tgz#24127054bb3f9bdcb4b1fb82418186072f77b8ce" - integrity sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w== +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== dependencies: function-bind "^1.1.1" - get-intrinsic "^1.0.0" + get-intrinsic "^1.0.2" call-me-maybe@^1.0.1: version "1.0.1" @@ -2878,14 +2861,14 @@ 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.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.15.0.tgz#3e2b319b6eb83be6094be166470158b186abdd04" - integrity sha512-2xXzYKUYrl79m1sertY+NL62T15Q5m1RLGuf5K8ZxX0gik0Ok3AmEhhEpSUaFBS48ocjHZ1rg5EgKK2A+7CY3g== +codemaker@^1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.20.1.tgz#5ac56a9c3f2b290ebd57538b340ba64fc908cdc1" + integrity sha512-AhDdoC0jhSw8VhbHBH2WN8CDp8PZKnO0YMAlZXG9BRLJnu4ztOQkYuB8BWipNydRHS3Zj1LQX4lPtTQkJN4vWg== dependencies: camelcase "^6.2.0" - decamelize "^4.0.0" - fs-extra "^9.0.1" + decamelize "^5.0.0" + fs-extra "^9.1.0" collect-v8-coverage@^1.0.0: version "1.0.1" @@ -2959,10 +2942,10 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -commonmark@^0.29.2: - version "0.29.2" - resolved "https://registry.yarnpkg.com/commonmark/-/commonmark-0.29.2.tgz#e7bd5582400f2a45421f2f64eca19fc89cbd4e1b" - integrity sha512-spe43MvEIaPpHss1T7z4yQaFQfLGmMu+yvCwv6xqhELIwkG/ZGgDpxOPzKxnuYzYT2c+aziCCc8m2rBVLA7jUA== +commonmark@^0.29.3: + version "0.29.3" + resolved "https://registry.yarnpkg.com/commonmark/-/commonmark-0.29.3.tgz#bb1d5733bfe3ea213b412f33f16439cc12999c2c" + integrity sha512-fvt/NdOFKaL2gyhltSy6BC4LxbbxbnPxBMl923ittqO/JBM0wQHaoYZliE4tp26cRxX/ZZtRsJlZzQrVdUkXAA== dependencies: entities "~2.0" mdurl "~1.0.1" @@ -3031,16 +3014,16 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= constructs@^3.2.0: - version "3.2.30" - resolved "https://registry.yarnpkg.com/constructs/-/constructs-3.2.30.tgz#950a1e38d7193791fea55f847f87013959748475" - integrity sha512-tzWUxXc9UjPbw1+c0s6gFL0ae4gPgsKck59xfkjOnyezPNcG2myB/xh9wGD51kbn+GInW0vMgW0QWOn0WhKa4g== + version "3.3.12" + resolved "https://registry.yarnpkg.com/constructs/-/constructs-3.3.12.tgz#9bbc8f121c9686195b4871749d91dfacba71acdb" + integrity sha512-K2Q1qYDYYGCCFBbWLNU5ja/mrHh2tB4SpSXU/XBfNxVl20OZTXUwzlFaCfSlBL12+dNbXC6f4eTpHZ2t5o7gDg== contains-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= -conventional-changelog-angular@^5.0.11, conventional-changelog-angular@^5.0.12, conventional-changelog-angular@^5.0.3: +conventional-changelog-angular@^5.0.12, conventional-changelog-angular@^5.0.3: 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== @@ -3048,7 +3031,7 @@ conventional-changelog-angular@^5.0.11, conventional-changelog-angular@^5.0.12, compare-func "^2.0.0" q "^1.5.1" -conventional-changelog-atom@^2.0.7, conventional-changelog-atom@^2.0.8: +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== @@ -3066,7 +3049,7 @@ conventional-changelog-cli@^2.1.1: meow "^8.0.0" tempfile "^3.0.0" -conventional-changelog-codemirror@^2.0.7, conventional-changelog-codemirror@^2.0.8: +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== @@ -3078,16 +3061,7 @@ conventional-changelog-config-spec@2.1.0: resolved "https://registry.yarnpkg.com/conventional-changelog-config-spec/-/conventional-changelog-config-spec-2.1.0.tgz#874a635287ef8b581fd8558532bf655d4fb59f2d" integrity sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ== -conventional-changelog-conventionalcommits@4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.4.0.tgz#8d96687141c9bbd725a89b95c04966d364194cd4" - integrity sha512-ybvx76jTh08tpaYrYn/yd0uJNLt5yMrb1BphDe4WBredMlvPisvMghfpnJb6RmRNcqXeuhR6LfGZGewbkRm9yA== - dependencies: - compare-func "^2.0.0" - lodash "^4.17.15" - q "^1.5.1" - -conventional-changelog-conventionalcommits@^4.4.0, conventional-changelog-conventionalcommits@^4.5.0: +conventional-changelog-conventionalcommits@4.5.0, 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== @@ -3115,17 +3089,17 @@ conventional-changelog-core@^3.1.6: read-pkg-up "^3.0.0" through2 "^3.0.0" -conventional-changelog-core@^4.2.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== +conventional-changelog-core@^4.2.1: + version "4.2.2" + resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.2.tgz#f0897df6d53b5d63dec36b9442bd45354f8b3ce5" + integrity sha512-7pDpRUiobQDNkwHyJG7k9f6maPo9tfPzkSWbRq97GGiZqisElhnvUZSvyQH20ogfOjntB5aadvv6NNcKL1sReg== 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-raw-commits "^2.0.8" git-remote-origin-url "^2.0.0" git-semver-tags "^4.1.1" lodash "^4.17.15" @@ -3136,35 +3110,35 @@ conventional-changelog-core@^4.2.0, conventional-changelog-core@^4.2.1: shelljs "^0.8.3" through2 "^4.0.0" -conventional-changelog-ember@^2.0.8, conventional-changelog-ember@^2.0.9: +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, conventional-changelog-eslint@^3.0.9: +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, conventional-changelog-express@^2.0.6: +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, conventional-changelog-jquery@^3.0.11: +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, conventional-changelog-jshint@^2.0.9: +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== @@ -3178,9 +3152,9 @@ conventional-changelog-preset-loader@^2.1.1, conventional-changelog-preset-loade integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== conventional-changelog-writer@^4.0.18, conventional-changelog-writer@^4.0.6: - version "4.0.18" - resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-4.0.18.tgz#10b73baa59c7befc69b360562f8b9cd19e63daf8" - integrity sha512-mAQDCKyB9HsE8Ko5cCM1Jn1AWxXPYV0v8dFPabZRkvsiWUul2YyAqbIaoMKF88Zf2ffnOPSvKhboLf3fnjo5/A== + version "4.1.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-4.1.0.tgz#1ca7880b75aa28695ad33312a1f2366f4b12659f" + integrity sha512-WwKcUp7WyXYGQmkLsX4QmU42AZ1lqlvRW9mqoyiQzdD+rJWbTepdWoKJuwXTS+yq79XKnQNa93/roViPQrAQgw== dependencies: compare-func "^2.0.0" conventional-commits-filter "^2.0.7" @@ -3193,24 +3167,7 @@ conventional-changelog-writer@^4.0.18, conventional-changelog-writer@^4.0.6: 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== - dependencies: - conventional-changelog-angular "^5.0.11" - conventional-changelog-atom "^2.0.7" - conventional-changelog-codemirror "^2.0.7" - conventional-changelog-conventionalcommits "^4.4.0" - conventional-changelog-core "^4.2.0" - conventional-changelog-ember "^2.0.8" - conventional-changelog-eslint "^3.0.8" - conventional-changelog-express "^2.0.5" - conventional-changelog-jquery "^3.0.10" - conventional-changelog-jshint "^2.0.8" - conventional-changelog-preset-loader "^2.3.4" - -conventional-changelog@^3.1.24: +conventional-changelog@3.1.24, 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== @@ -3227,7 +3184,7 @@ conventional-changelog@^3.1.24: 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, conventional-commits-filter@^2.0.7: +conventional-commits-filter@^2.0.2, 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== @@ -3235,7 +3192,7 @@ conventional-commits-filter@^2.0.2, conventional-commits-filter@^2.0.6, conventi lodash.ismatch "^4.4.0" modify-values "^1.0.0" -conventional-commits-parser@^3.0.3, conventional-commits-parser@^3.1.0, conventional-commits-parser@^3.2.0: +conventional-commits-parser@^3.0.3, 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== @@ -3248,18 +3205,18 @@ conventional-commits-parser@^3.0.3, conventional-commits-parser@^3.1.0, conventi 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" - integrity sha512-2ibrqAFMN3ZA369JgVoSbajdD/BHN6zjY7DZFKTHzyzuQejDUCjQ85S5KHxCRxNwsbDJhTPD5hOKcis/jQhRgg== +conventional-recommended-bump@6.0.11: + version "6.0.11" + resolved "https://registry.yarnpkg.com/conventional-recommended-bump/-/conventional-recommended-bump-6.0.11.tgz#fcc39acb51d1946b63fc478737d1e52712f36356" + integrity sha512-FciYBMwzwwBZ1K4NS8c57rsOfSc51e1V6UVSNIosrjH+A6xXkyiA4ELwoWyRKdMhJ+m3O6ru9ZJ7F2QFjjYJdQ== dependencies: concat-stream "^2.0.0" conventional-changelog-preset-loader "^2.3.4" - conventional-commits-filter "^2.0.6" - conventional-commits-parser "^3.1.0" + conventional-commits-filter "^2.0.7" + conventional-commits-parser "^3.2.0" git-raw-commits "2.0.0" - git-semver-tags "^4.1.0" - meow "^7.0.0" + git-semver-tags "^4.1.1" + meow "^8.0.0" q "^1.5.1" conventional-recommended-bump@^5.0.0: @@ -3346,9 +3303,9 @@ crc-32@^1.2.0: printj "~1.1.0" crc32-stream@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.1.tgz#0f047d74041737f8a55e86837a1b826bd8ab0067" - integrity sha512-FN5V+weeO/8JaXsamelVYO1PHyeCsuL3HcG4cqsj0ceARcocxalaShCsohZMSAF+db7UYFwBy1rARK/0oFItUw== + version "4.0.2" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.2.tgz#c922ad22b38395abe9d3870f02fa8134ed709007" + integrity sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w== dependencies: crc-32 "^1.2.0" readable-stream "^3.4.0" @@ -3427,6 +3384,11 @@ dargs@^4.0.1: dependencies: number-is-nan "^1.0.0" +dargs@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" + integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -3463,7 +3425,7 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -debug@3.1.0, debug@=3.1.0: +debug@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -3471,9 +3433,9 @@ debug@3.1.0, debug@=3.1.0: ms "2.0.0" debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" - integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== + version "4.3.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" + integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== dependencies: ms "2.1.2" @@ -3485,9 +3447,9 @@ debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: ms "2.0.0" debug@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" @@ -3509,11 +3471,6 @@ decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= -decamelize@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" - integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== - decamelize@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-5.0.0.tgz#88358157b010ef133febfd27c18994bd80c6215b" @@ -3534,20 +3491,21 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= -deep-equal@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.4.tgz#6b0b407a074666033169df3acaf128e1c6f3eab6" - integrity sha512-BUfaXrVoCfgkOQY/b09QdO9L3XNoF2XH0A3aY9IQwQL/ZjLOe8FQgCNVl1wiolhsFo8kFdO9zdPViCPbmaJA5w== +deep-equal@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.5.tgz#55cd2fe326d83f9cbf7261ef0e060b3f724c5cb9" + integrity sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw== dependencies: - es-abstract "^1.18.0-next.1" - es-get-iterator "^1.1.0" + call-bind "^1.0.0" + es-get-iterator "^1.1.1" + get-intrinsic "^1.0.1" is-arguments "^1.0.4" is-date-object "^1.0.2" is-regex "^1.1.1" isarray "^2.0.5" - object-is "^1.1.3" + object-is "^1.1.4" object-keys "^1.1.1" - object.assign "^4.1.1" + object.assign "^4.1.2" regexp.prototype.flags "^1.3.0" side-channel "^1.0.3" which-boxed-primitive "^1.0.1" @@ -3628,10 +3586,10 @@ degenerator@^2.2.0: escodegen "^1.8.1" esprima "^4.0.0" -delay@4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/delay/-/delay-4.4.0.tgz#71abc745f3ce043fe7f450491236541edec4ad0c" - integrity sha512-txgOrJu3OdtOfTiEOT2e76dJVfG/1dz2NZ4F0Pyt4UGZJryssMRp5vdM5wQoLwSOBNdrJv3F9PAhp/heqd7vrA== +delay@4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/delay/-/delay-4.4.1.tgz#6e02d02946a1b6ab98b39262ced965acba2ac4d1" + integrity sha512-aL3AhqtfhOlT/3ai6sWXeqwnw63ATNpnUiN4HL7x9q+My5QtHlO3OIkasmug9LKzpheLdmUKGRKnYXYAS7FQkQ== delayed-stream@~1.0.0: version "1.0.0" @@ -3758,6 +3716,16 @@ dot-prop@^5.1.0: dependencies: is-obj "^2.0.0" +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@^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" @@ -3848,9 +3816,9 @@ env-paths@^2.2.0: integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA== envinfo@^7.3.1: - version "7.7.3" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.7.3.tgz#4b2d8622e3e7366afb8091b23ed95569ea0208cc" - integrity sha512-46+j5QxbPWza0PB1i15nZx0xQ4I/EfQxg9J8Had3b408SV63nEtor2e+oiY63amTo9KTuh2a3XLObNwduxYwwA== + version "7.7.4" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.7.4.tgz#c6311cdd38a0e86808c1c9343f667e4267c4a320" + integrity sha512-TQXTYFVVwwluWSFis6K2XKxgrD22jEv0FTuLCQI+OjH7rn93+iY0fSSFM5lrSxFY+H1+B0/cvvlamr3UsBivdQ== err-code@^1.0.0: version "1.1.2" @@ -3864,52 +3832,37 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5: - version "1.17.7" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" - integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-abstract@^1.18.0-next.0, es-abstract@^1.18.0-next.1: - version "1.18.0-next.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" - integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== +es-abstract@^1.18.0-next.1: + version "1.18.0-next.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.2.tgz#088101a55f0541f595e7e057199e27ddc8f3a5c2" + integrity sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw== dependencies: + call-bind "^1.0.2" es-to-primitive "^1.2.1" function-bind "^1.1.1" + get-intrinsic "^1.0.2" has "^1.0.3" has-symbols "^1.0.1" is-callable "^1.2.2" - is-negative-zero "^2.0.0" + is-negative-zero "^2.0.1" is-regex "^1.1.1" - object-inspect "^1.8.0" + object-inspect "^1.9.0" object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.3" + string.prototype.trimstart "^1.0.3" -es-get-iterator@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.1.tgz#b93ddd867af16d5118e00881396533c1c6647ad9" - integrity sha512-qorBw8Y7B15DVLaJWy6WdEV/ZkieBcu6QCq/xzWzGOKJqgG1j754vXRfZ3NY7HSShneqU43mPB4OkQBTkvHhFw== +es-get-iterator@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.2.tgz#9234c54aba713486d7ebde0220864af5e2b283f7" + integrity sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ== dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.1" + call-bind "^1.0.2" + get-intrinsic "^1.1.0" has-symbols "^1.0.1" - is-arguments "^1.0.4" - is-map "^2.0.1" - is-set "^2.0.1" + is-arguments "^1.1.0" + is-map "^2.0.2" + is-set "^2.0.2" is-string "^1.0.5" isarray "^2.0.5" @@ -3944,10 +3897,10 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -esbuild@^0.8.34: - version "0.8.34" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.34.tgz#16b4ac58f74c821d2c5a8c2f0585ca96a38ab4e6" - integrity sha512-tnr0V1ooakYr1aRLXQLzCn2GVG1kBTW3FWpRyC+NgrR3ntsouVpJOlTOV0BS4YLATx3/c+x3h/uBq9lWJlUAtQ== +esbuild@^0.8.44: + version "0.8.44" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.44.tgz#2a74f48fe20579081c9d8fe99be6fb8d2848c887" + integrity sha512-m9yyBZMgWuAB7e7tA2g9L4PovoLa5Xb73+Yg9uBBR2w3Fe4P9/nxqj/HLrw1k/rjdjF1eX1kNJRytboqOtRCCQ== escalade@^3.1.1: version "3.1.1" @@ -3981,6 +3934,11 @@ 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" @@ -4008,6 +3966,14 @@ 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" @@ -4034,11 +4000,33 @@ eslint-plugin-jest@^24.1.3: dependencies: "@typescript-eslint/experimental-utils" "^4.0.1" +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.3.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.3.1.tgz#61485df2a359e03149fdafc0a68b0e030ad2ac45" + integrity sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ== + 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.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.1.0.tgz#0c3bf3a67e853f8bbbc580fb4945fbf16f41b7c5" + integrity sha512-ZL7+QRixjTR6/528YNGyDotyffm5OQst/sGxKDwGb9Uqs4In5Egi4+jbobhqJoyoCM6/7v/1A5fhQ7ScMtDjaQ== + 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" @@ -4064,13 +4052,13 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint@^7.13.0: - version "7.13.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.13.0.tgz#7f180126c0dcdef327bfb54b211d7802decc08da" - integrity sha512-uCORMuOO8tUzJmsdRtrvcGq5qposf7Rw0LwkTJkoDbOycVQtQjmnhZSuLQnozLE4TmAzlMVV45eCHmQ1OpDKUQ== +eslint@^7.19.0: + version "7.19.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.19.0.tgz#6719621b196b5fad72e43387981314e5d0dc3f41" + integrity sha512-CGlMgJY56JZ9ZSYhJuhow61lMPPjUzWmChFya71Z/jilVos7mR/jPgaEfVGgMBY5DshbKdG8Ezb8FDCHcoMEMg== dependencies: "@babel/code-frame" "^7.0.0" - "@eslint/eslintrc" "^0.2.1" + "@eslint/eslintrc" "^0.3.0" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -4080,10 +4068,10 @@ eslint@^7.13.0: eslint-scope "^5.1.1" eslint-utils "^2.1.0" eslint-visitor-keys "^2.0.0" - espree "^7.3.0" + espree "^7.3.1" esquery "^1.2.0" esutils "^2.0.2" - file-entry-cache "^5.0.1" + file-entry-cache "^6.0.0" functional-red-black-tree "^1.0.1" glob-parent "^5.0.0" globals "^12.1.0" @@ -4094,7 +4082,7 @@ eslint@^7.13.0: js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" - lodash "^4.17.19" + lodash "^4.17.20" minimatch "^3.0.4" natural-compare "^1.4.0" optionator "^0.9.1" @@ -4103,7 +4091,7 @@ eslint@^7.13.0: semver "^7.2.1" strip-ansi "^6.0.0" strip-json-comments "^3.1.0" - table "^5.2.3" + table "^6.0.4" text-table "^0.2.0" v8-compile-cache "^2.0.3" @@ -4112,13 +4100,13 @@ esm@^3.2.5: resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10" integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== -espree@^7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348" - integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw== +espree@^7.3.0, espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== dependencies: acorn "^7.4.0" - acorn-jsx "^5.2.0" + acorn-jsx "^5.3.1" eslint-visitor-keys "^1.3.0" esprima@^4.0.0, esprima@^4.0.1: @@ -4127,9 +4115,9 @@ esprima@^4.0.0, esprima@^4.0.1: integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esquery@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" - integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== dependencies: estraverse "^5.1.0" @@ -4291,10 +4279,10 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fast-check@^2.11.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-2.11.0.tgz#a21bdbdcab27812fbf93612f3c84483883115ca2" - integrity sha512-galBVrbyjdHOW+WOCp/NFP3J6t6Pc0uajz0oJaUAFRXLHXt6lcUeD1bcBFqUWV1aeK9QJgeRpIYf4e+PHeASUQ== +fast-check@^2.12.1: + version "2.12.1" + resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-2.12.1.tgz#457d13e36952dd0d660cf781d942a43c067a47ca" + integrity sha512-zXCxkvFVlVWbBVky+TPaD1H+g6SC4R7IQYZ5gcxU3lUKHyrOFiLXtWy+xHVDmm9bv+3NJbSzOmeVvhgpnjR7Sw== dependencies: pure-rand "^4.1.1" @@ -4321,9 +4309,9 @@ fast-glob@^2.2.6: micromatch "^3.1.10" fast-glob@^3.1.1: - version "3.2.4" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" - integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== + version "3.2.5" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" + integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -4355,9 +4343,9 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= fastq@^1.6.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.9.0.tgz#e16a72f338eaca48e91b5c23593bcc2ef66b7947" - integrity sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w== + version "1.10.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.1.tgz#8b8f2ac8bf3632d67afcd65dac248d5fdc45385e" + integrity sha512-AWuv6Ery3pM+dY7LYS8YIaCiQvUaos9OB1RyNgaOWnaX+Tik7Onvcsf8x8c+YtDeT0maYLniBip2hox5KtEXXA== dependencies: reusify "^1.0.4" @@ -4387,12 +4375,12 @@ figures@^3.1.0: dependencies: escape-string-regexp "^1.0.5" -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== +file-entry-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.0.tgz#7921a89c391c6d93efec2169ac6bf300c527ea0a" + integrity sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA== dependencies: - flat-cache "^2.0.1" + flat-cache "^3.0.4" file-uri-to-path@2: version "2.0.0" @@ -4472,20 +4460,32 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" + flatted "^3.1.0" + rimraf "^3.0.2" -flatted@^2.0.0, flatted@^2.0.1: +flatted@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +flatted@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.1.1.tgz#c4b489e80096d9df1dfc97c79871aea7c617c469" + integrity sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA== + flush-write-stream@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" @@ -4494,17 +4494,10 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" - integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== - dependencies: - debug "=3.1.0" - -follow-redirects@^1.11.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" - integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== +follow-redirects@^1.10.0, follow-redirects@^1.11.0: + version "1.13.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.2.tgz#dd73c8effc12728ba5cf4259d760ea5fb83e3147" + integrity sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA== for-in@^1.0.2: version "1.0.2" @@ -4592,7 +4585,7 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.1, fs-extra@^9.1.0: +fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -4625,9 +4618,9 @@ fs.realpath@^1.0.0: integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@^2.1.2: - version "2.2.1" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.2.1.tgz#1fb02ded2036a8ac288d507a65962bd87b97628d" - integrity sha512-bTLYHSeC0UH/EFXS9KqWnXuOl/wHK5Z/d+ghd5AsFMYN7wIGkUCOJyzy88+wJKkZPGON8u4Z9f6U4FdgURE9qA== + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== ftp@^0.3.10: version "0.3.10" @@ -4681,10 +4674,10 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-intrinsic@^1.0.0, get-intrinsic@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.1.tgz#94a9768fcbdd0595a1c9273aacf4c89d075631be" - integrity sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg== +get-intrinsic@^1.0.1, get-intrinsic@^1.0.2, get-intrinsic@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== dependencies: function-bind "^1.1.1" has "^1.0.3" @@ -4770,6 +4763,17 @@ git-raw-commits@2.0.0: split2 "^2.0.0" through2 "^2.0.0" +git-raw-commits@^2.0.8: + version "2.0.10" + resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.10.tgz#e2255ed9563b1c9c3ea6bd05806410290297bbc1" + integrity sha512-sHhX5lsbG9SOO6yXdlwgEMQ/ljIn7qMpAbJZCGfXX2fq5T8M5SrDnpYk9/4HswTildcIqatsWa91vty6VhWSaQ== + dependencies: + dargs "^7.0.0" + lodash "^4.17.15" + meow "^8.0.0" + split2 "^3.0.0" + through2 "^4.0.0" + git-remote-origin-url@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz#5282659dae2107145a11126112ad3216ec5fa65f" @@ -4786,7 +4790,7 @@ git-semver-tags@^2.0.3: meow "^4.0.0" semver "^6.0.0" -git-semver-tags@^4.0.0, git-semver-tags@^4.1.0, git-semver-tags@^4.1.1: +git-semver-tags@^4.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== @@ -4803,9 +4807,9 @@ git-up@^4.0.0: parse-url "^5.0.0" git-url-parse@^11.1.2: - version "11.4.0" - resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-11.4.0.tgz#f2bb1f2b00f05552540e95a62e31399a639a6aa6" - integrity sha512-KlIa5jvMYLjXMQXkqpFzobsyD/V2K5DRHl5OAf+6oDFPlPLxrGDVQlIdI63c4/Kt6kai4kALENSALlzTGST3GQ== + version "11.4.4" + resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77" + integrity sha512-Y4o9o7vQngQDIU9IjyCmRJBin5iYjI5u9ZITnddRZpD7dcCFQj2sL2XuMNbLRE4b4B/4ENPsp2Q8P44fjAZ0Pw== dependencies: git-up "^4.0.0" @@ -4816,12 +4820,12 @@ gitconfiglocal@^1.0.0: dependencies: ini "^1.3.2" -github-api@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/github-api/-/github-api-3.3.0.tgz#5aaa2cc6787ff5bc27b3d01cba8bddfa7a7f6aad" - integrity sha512-30pABj/1ciHmlqmjnWXn+A4JL8j9qB2IcQgibrJ7euGbaNRkAj+T6QhJwjLcPx4Hxlj+BP1TcdvaQ/7resw+VA== +github-api@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/github-api/-/github-api-3.4.0.tgz#5da2f56442d4839d324e9faf0ffb2cf30f7650b8" + integrity sha512-2yYqYS6Uy4br1nw0D3VrlYWxtGTkUhIZrumBrcBwKdBOzMT8roAe8IvI6kjIOkxqxapKR5GkEsHtz3Du/voOpA== dependencies: - axios "^0.19.0" + axios "^0.21.1" debug "^2.2.0" js-base64 "^2.1.9" utf8 "^2.1.1" @@ -4871,9 +4875,9 @@ globals@^12.1.0: type-fest "^0.8.1" globby@^11.0.1: - version "11.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" - integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== + version "11.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83" + integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og== dependencies: array-union "^2.1.0" dir-glob "^3.0.1" @@ -4896,10 +4900,10 @@ globby@^9.2.0: pify "^4.0.1" slash "^2.0.0" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: - version "4.2.4" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" - integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== growly@^1.3.0: version "1.3.0" @@ -5020,9 +5024,9 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: 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== + version "3.0.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.8.tgz#6e35d4cc87af2c5f816e4cb9ce350ba87a3f370d" + integrity sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw== dependencies: lru-cache "^6.0.0" @@ -5149,7 +5153,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.4, ignore@^5.1.8, ignore@~5.1.8: +ignore@^5.1.1, ignore@^5.1.4, ignore@^5.1.8, ignore@~5.1.8: version "5.1.8" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== @@ -5168,9 +5172,9 @@ import-fresh@^2.0.0: resolve-from "^3.0.0" import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.2.2" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.2.tgz#fc129c160c5d68235507f4331a6baad186bdbc3e" - integrity sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw== + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== dependencies: parent-module "^1.0.0" resolve-from "^4.0.0" @@ -5232,9 +5236,9 @@ inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, i integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: - version "1.3.7" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" - integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== init-package-json@^1.10.3: version "1.10.3" @@ -5298,25 +5302,29 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" -is-arguments@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" - integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== +is-arguments@^1.0.4, is-arguments@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9" + integrity sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg== + dependencies: + call-bind "^1.0.0" is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= -is-bigint@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" - integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== - -is-boolean-object@^1.0.0: +is-bigint@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" - integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.1.tgz#6923051dfcbc764278540b9ce0e6b3213aa5ebc2" + integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg== + +is-boolean-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0" + integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA== + dependencies: + call-bind "^1.0.0" is-buffer@^1.1.5, is-buffer@~1.1.6: version "1.1.6" @@ -5324,9 +5332,9 @@ is-buffer@^1.1.5, is-buffer@~1.1.6: integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== is-callable@^1.1.4, is-callable@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" - integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== + version "1.2.3" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" + integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== is-ci@^2.0.0: version "2.0.0" @@ -5335,7 +5343,7 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-core-module@^2.0.0: +is-core-module@^2.1.0, is-core-module@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== @@ -5447,17 +5455,17 @@ is-glob@^4.0.0, is-glob@^4.0.1: dependencies: is-extglob "^2.1.1" -is-map@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" - integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== +is-map@^2.0.1, is-map@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== -is-negative-zero@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" - integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= +is-negative-zero@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" + integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== -is-number-object@^1.0.3: +is-number-object@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== @@ -5485,9 +5493,9 @@ is-obj@^2.0.0: integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== is-object@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" - integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA= + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf" + integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA== is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: version "1.1.0" @@ -5512,16 +5520,17 @@ is-potential-custom-element-name@^1.0.0: integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= is-regex@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" + integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== dependencies: + call-bind "^1.0.2" has-symbols "^1.0.1" -is-set@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" - integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== +is-set@^2.0.1, is-set@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== is-ssh@^1.3.0: version "1.3.2" @@ -5540,12 +5549,12 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== -is-string@^1.0.4, is-string@^1.0.5: +is-string@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== -is-symbol@^1.0.2: +is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== @@ -5560,12 +5569,13 @@ is-text-path@^1.0.1: text-extensions "^1.0.0" is-typed-array@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d" - integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ== + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.4.tgz#1f66f34a283a3c94a4335434661ca53fff801120" + integrity sha512-ILaRgn4zaSrVNXNGtON6iFNotXW3hAPF3+0fB1usg2jFlWqo5fEDdmJkz0zBfoi7Dgskr8Khi2xZ8cXqZEfXNA== dependencies: - available-typed-arrays "^1.0.0" - es-abstract "^1.17.4" + available-typed-arrays "^1.0.2" + call-bind "^1.0.0" + es-abstract "^1.18.0-next.1" foreach "^2.0.5" has-symbols "^1.0.1" @@ -6125,7 +6135,7 @@ jest-worker@^26.6.2: merge-stream "^2.0.0" supports-color "^7.0.0" -jest@^26.6.0, jest@^26.6.3: +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== @@ -6199,76 +6209,76 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsii-diff@^1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.15.0.tgz#77e03f5c5557ba20d9bc2cc354a7f03bb93f21f0" - integrity sha512-T47ogWBdztPrOhy53ngm9ZBF0gYz89BcNnM8WJAET6GcpO5qyoAwpQpf4WuA9oDdX8Q9yV1xOHPtBDD+7PmeFQ== +jsii-diff@^1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.20.1.tgz#ea7a6ded96b896287a1f36198d235bf1eb7420a4" + integrity sha512-5k6W9Xi0zhX9yHYhHBkhKrRo76ydcVKSRGJCaS7t4qtZut8mAsEXU9dleUhxhILXMhJ59XBEw8nMgp71wzNKdQ== dependencies: - "@jsii/spec" "^1.15.0" - fs-extra "^9.0.1" - jsii-reflect "^1.15.0" + "@jsii/spec" "^1.20.1" + fs-extra "^9.1.0" + jsii-reflect "^1.20.1" log4js "^6.3.0" typescript "~3.9.7" - yargs "^16.1.1" + yargs "^16.2.0" -jsii-pacmak@^1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.15.0.tgz#48060233cfe693a24554c0e28a851aa1f690c734" - integrity sha512-/VxBDjC7Mi3zhuopm1oL0mXNBuC32YhuQJnHyHkFEnFduso7gnc6/3OCQbm3l5pOxHfg7oTBXIKxwDkh7EkK0w== +jsii-pacmak@^1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.20.1.tgz#9f5b1a816b560afa577d42fb573b643921fc1edb" + integrity sha512-mNViMzB+wqDKowjmcOMa4qLmkhZ4GAs2wOjNiHcWTN9ZtdyjG5BoRgJrKChlRYQfjRuE+EmqiHpZyH8IzPo8SQ== dependencies: - "@jsii/spec" "^1.15.0" + "@jsii/spec" "^1.20.1" clone "^2.1.2" - codemaker "^1.15.0" - commonmark "^0.29.2" + codemaker "^1.20.1" + commonmark "^0.29.3" escape-string-regexp "^4.0.0" - fs-extra "^9.0.1" - jsii-reflect "^1.15.0" - jsii-rosetta "^1.15.0" - semver "^7.3.2" - spdx-license-list "^6.3.0" + fs-extra "^9.1.0" + jsii-reflect "^1.20.1" + jsii-rosetta "^1.20.1" + semver "^7.3.4" + spdx-license-list "^6.4.0" xmlbuilder "^15.1.1" - yargs "^16.1.1" + yargs "^16.2.0" -jsii-reflect@^1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.15.0.tgz#a65297a581b2b7bfbaa213d0bca7f76c693f718e" - integrity sha512-8mKV5OVt/FwRdLNmr7By5T0HyhjjLlHsE5oiOta6NdkdMB7magjzZ1bucenJ2DdvPidX2dP6IDujfOKoXVNsjA== +jsii-reflect@^1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.20.1.tgz#25b3146e923e728202c8fcac1c611eb261fa3a6c" + integrity sha512-TkimkYu25H6YtfKRz7D/ZXbquXbhylS60TxgSPZC85iItCpZMwTlbIdvnpBKasx8e7MtJO6xYoAHEYHRhs8djQ== dependencies: - "@jsii/spec" "^1.15.0" + "@jsii/spec" "^1.20.1" colors "^1.4.0" - fs-extra "^9.0.1" - oo-ascii-tree "^1.15.0" - yargs "^16.1.1" - -jsii-rosetta@^1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.15.0.tgz#a092591662e4c2f39144562eafe30cbc0d935e13" - integrity sha512-0yXdm6X0IAsjzKDq0QO0n7EPVQ3vTW3qwgFH+ZNirqFLP5xqnpxXS1BtrQNP9zxC7gfacgsDS48MroagKQWDHg== - dependencies: - "@jsii/spec" "^1.15.0" - commonmark "^0.29.2" - fs-extra "^9.0.1" + fs-extra "^9.1.0" + oo-ascii-tree "^1.20.1" + yargs "^16.2.0" + +jsii-rosetta@^1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.20.1.tgz#ecba41af1862405a37ea974d09932f403a073c6c" + integrity sha512-3F01+B8zahWD4GhTMWPzlaGhuGvdPdYyIdhIe1bkHCYJuL1ttcf+ulCxVeF4qkBAxB2+/cfHBvEMHfSMoYCISA== + dependencies: + "@jsii/spec" "^1.20.1" + commonmark "^0.29.3" + fs-extra "^9.1.0" typescript "~3.9.7" xmldom "^0.4.0" - yargs "^16.1.1" + yargs "^16.2.0" -jsii@^1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.15.0.tgz#3f3160ee2c5fe62473855c1ac8d2aa9e3bf72ad1" - integrity sha512-kYiO1WkeqN7or4rz5ccYooO576+wiqiRGzv+UCI6hShKd42ff3xYZ1oTUSnBQdh9lp9i/nlNtx7KGUEqjC16Aw== +jsii@^1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.20.1.tgz#e41b32a3968e781aec1348099e441c3fa5509639" + integrity sha512-e4LS9u4QhhsqDN/JlK7JHDhYXhg3fiw+nq1xU5EeAtHQJvmM7CXTXhgDT9FjtnkWbnnjCOd0hgLcE8yIIZ8Lkw== dependencies: - "@jsii/spec" "^1.15.0" + "@jsii/spec" "^1.20.1" case "^1.6.3" colors "^1.4.0" - deep-equal "^2.0.4" - fs-extra "^9.0.1" + deep-equal "^2.0.5" + fs-extra "^9.1.0" log4js "^6.3.0" - semver "^7.3.2" + semver "^7.3.4" semver-intersect "^1.4.0" sort-json "^2.0.0" - spdx-license-list "^6.3.0" + spdx-license-list "^6.4.0" typescript "~3.9.7" - yargs "^16.1.1" + yargs "^16.2.0" json-diff@^0.5.4: version "0.5.4" @@ -6322,9 +6332,9 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= json5@2.x, json5@^2.1.2: - version "2.1.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43" - integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA== + version "2.2.0" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" + integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== dependencies: minimist "^1.2.5" @@ -6425,6 +6435,24 @@ 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" @@ -6566,6 +6594,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -6616,11 +6651,6 @@ lodash.isplainobject@^4.0.6: resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= -lodash.memoize@4.x: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= - lodash.set@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" @@ -6656,7 +6686,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1: +lodash@4.x, lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -6886,27 +6916,10 @@ meow@^4.0.0: redent "^2.0.0" trim-newlines "^2.0.0" -meow@^7.0.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.1.tgz#7c01595e3d337fcb0ec4e8eed1666ea95903d306" - integrity sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA== - 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 "^2.5.0" - read-pkg-up "^7.0.1" - redent "^3.0.0" - trim-newlines "^3.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== + version "8.1.2" + resolved "https://registry.yarnpkg.com/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" + integrity sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q== dependencies: "@types/minimist" "^1.2.0" camelcase-keys "^6.2.2" @@ -6969,17 +6982,17 @@ micromatch@^4.0.2: braces "^3.0.1" picomatch "^2.0.5" -mime-db@1.44.0: - version "1.44.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" - integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== +mime-db@1.45.0: + version "1.45.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.45.0.tgz#cceeda21ccd7c3a745eba2decd55d4b73e7879ea" + integrity sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w== mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.27" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" - integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + version "2.1.28" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.28.tgz#1160c4757eab2c5363888e005273ecf79d2a0ecd" + integrity sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ== dependencies: - mime-db "1.44.0" + mime-db "1.45.0" mimic-fn@^1.0.0: version "1.2.0" @@ -7127,11 +7140,16 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@2.1.2, ms@^2.0.0, ms@^2.1.1: +ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.0.0, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + multimatch@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-3.0.0.tgz#0e2534cc6bc238d9ab67e1b9cd5fcd85a6dbf70b" @@ -7214,10 +7232,10 @@ nise@^4.0.4: just-extend "^4.0.2" path-to-regexp "^1.7.0" -nock@^13.0.5: - version "13.0.5" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.5.tgz#a618c6f86372cb79fac04ca9a2d1e4baccdb2414" - integrity sha512-1ILZl0zfFm2G4TIeJFW0iHknxr2NyA+aGCMTjDVUsBY4CkMRispF1pfIYkTRdAR/3Bg+UzdEuK0B6HczMQZcCg== +nock@^13.0.7: + version "13.0.7" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.7.tgz#9bc718c66bd0862dfa14601a9ba678a406127910" + integrity sha512-WBz73VYIjdbO6BwmXODRQLtn7B5tldA9pNpWJe5QTtTEscQlY5KXU4srnGzBOK2fWakkXj69gfTnXGzmrsaRWw== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" @@ -7513,18 +7531,18 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" -object-inspect@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== +object-inspect@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" + integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== -object-is@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.3.tgz#2e3b9e65560137455ee3bd62aec4d90a2ea1cc81" - integrity sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg== +object-is@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.4.tgz#63d6c83c00a43f4cbc9434eb9757c8a5b8565068" + integrity sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg== dependencies: + call-bind "^1.0.0" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" @@ -7538,7 +7556,7 @@ object-visit@^1.0.0: dependencies: isobject "^3.0.0" -object.assign@^4.1.1: +object.assign@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== @@ -7549,12 +7567,13 @@ object.assign@^4.1.1: object-keys "^1.1.1" object.getownpropertydescriptors@^2.0.3: - version "2.1.0" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" - integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== + version "2.1.1" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz#0dfda8d108074d9c563e80490c883b6661091544" + integrity sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng== dependencies: + call-bind "^1.0.0" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" + es-abstract "^1.18.0-next.1" object.pick@^1.3.0: version "1.3.0" @@ -7564,13 +7583,13 @@ object.pick@^1.3.0: isobject "^3.0.1" object.values@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" - integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== + version "1.1.2" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.2.tgz#7a2015e06fcb0f546bd652486ce8583a4731c731" + integrity sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag== dependencies: + call-bind "^1.0.0" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" + es-abstract "^1.18.0-next.1" has "^1.0.3" octokit-pagination-methods@^1.1.0: @@ -7599,10 +7618,10 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -oo-ascii-tree@^1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.15.0.tgz#51227b6a0a8a1c933afe312556c696058fcc32a3" - integrity sha512-FR8ygFwcH9DBkQIcp/lAe49EFcTNMGjU3jgAsRaZ8ktNVxDM9EszlLNEO1K10QTHZwT3iZxq+E+KwT811B4ayw== +oo-ascii-tree@^1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.20.1.tgz#685317abe5000398374eede506cba57dcba6092a" + integrity sha512-imKW5pdztAEqtl5pmNTM9Rva53XOcuY91YL9jTuwFGJpBGqT6pnyxXJOFdwc5TRaQr9+GdpWTpByGRvgNPbdvQ== opener@^1.5.1: version "1.5.2" @@ -7672,9 +7691,9 @@ own-or@^1.0.0: integrity sha1-Tod/vtqaLsgAD7wLyuOWRe6L+Nw= p-each-series@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.1.0.tgz#961c8dd3f195ea96c747e636b262b800a6b1af48" - integrity sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ== + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" + integrity sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA== p-finally@^1.0.0: version "1.0.0" @@ -7695,6 +7714,13 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -7716,6 +7742,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map-series@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-map-series/-/p-map-series-1.0.0.tgz#bf98fe575705658a9e1351befb85ae4c1f07bdca" @@ -7855,9 +7888,9 @@ parse-json@^4.0.0: json-parse-better-errors "^1.0.1" parse-json@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646" - integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ== + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== dependencies: "@babel/code-frame" "^7.0.0" error-ex "^1.3.1" @@ -7865,12 +7898,14 @@ parse-json@^5.0.0: lines-and-columns "^1.1.6" parse-path@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-4.0.2.tgz#ef14f0d3d77bae8dd4bc66563a4c151aac9e65aa" - integrity sha512-HSqVz6iuXSiL8C1ku5Gl1Z5cwDd9Wo0q8CoffdAghP6bz8pJa1tcMC+m4N+z6VAS8QdksnIGq1TB6EgR4vPR6w== + version "4.0.3" + resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-4.0.3.tgz#82d81ec3e071dcc4ab49aa9f2c9c0b8966bb22bf" + integrity sha512-9Cepbp2asKnWTJ9x2kpw6Fe8y9JDbqwahGCTvklzd/cEq5C5JC59x2Xb0Kx+x0QZ8bvNquGO8/BWP0cwBHzSAA== dependencies: is-ssh "^1.3.0" protocols "^1.4.0" + qs "^6.9.4" + query-string "^6.13.8" parse-url@^5.0.0: version "5.0.2" @@ -8212,20 +8247,34 @@ punycode@^2.0.0, punycode@^2.1.0, punycode@^2.1.1: integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== pure-rand@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-4.1.1.tgz#9fca2d4af5c4e870bac337ed860977426ed17bf6" - integrity sha512-cZw4AL/KI6aDTdqHEbJPe2ZoHM3kSdpJRLJetv8c3tfq9o+PvQDXrHNEpB0AWukAGFx4fmeOerAGwkA4rtUgdA== + version "4.1.2" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-4.1.2.tgz#cbad2a3e3ea6df0a8d80d8ba204779b5679a5205" + integrity sha512-uLzZpQWfroIqyFWmX/pl0OL2JHJdoU3dbh0dvZ25fChHFJJi56J5oQZhW6QgbT2Llwh1upki84LnTwlZvsungA== q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qs@^6.9.4: + version "6.9.6" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" + integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ== + qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +query-string@^6.13.8: + version "6.13.8" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.8.tgz#8cf231759c85484da3cf05a851810d8e825c1159" + integrity sha512-jxJzQI2edQPE/NPUOusNjO/ZOGqr1o2OBa/3M00fU76FsLXDVbJDv/p7ng5OdQyorKrkRz1oqfwmbe5MAMePQg== + dependencies: + decode-uri-component "^0.2.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + querystring@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" @@ -8400,7 +8449,7 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -"readable-stream@2 || 3", readable-stream@3, 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== @@ -8466,12 +8515,12 @@ regex-not@^1.0.0, regex-not@^1.0.2: safe-regex "^1.1.0" regexp.prototype.flags@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" - integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== + version "1.3.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" + integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" regexpp@^3.0.0, regexpp@^3.1.0: version "3.1.0" @@ -8598,19 +8647,20 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.3.2: - version "1.17.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" - integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.18.1: + version "1.19.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" + integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== dependencies: + is-core-module "^2.1.0" path-parse "^1.0.6" -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== +resolve@^1.10.1: + version "1.20.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" + integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== dependencies: - is-core-module "^2.0.0" + is-core-module "^2.2.0" path-parse "^1.0.6" restore-cursor@^2.0.0: @@ -8637,16 +8687,9 @@ reusify@^1.0.4: integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== rfdc@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" - integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== - -rimraf@2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" + version "1.2.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.2.0.tgz#9e9894258f48f284b43c3143c68070a4f373b949" + integrity sha512-ijLyszTMmUrXvjSooucVQwimGUk84eRcmCuLV8Xghe3UO85mjUtRAHRyoMM6XtyqbECaXuBWx18La3523sXINA== rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: version "2.7.1" @@ -8655,7 +8698,7 @@ rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: dependencies: glob "^7.1.3" -rimraf@^3.0.0: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -8757,12 +8800,14 @@ semver-intersect@^1.4.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@7.x, semver@^7.1.1, semver@^7.2.1, semver@^7.3.2: - version "7.3.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" - integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== +semver@7.x, semver@^7.1.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4: + version "7.3.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" + integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== + dependencies: + lru-cache "^6.0.0" -semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: +semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, 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== @@ -8838,27 +8883,27 @@ shellwords@^0.1.1: integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== side-channel@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3" - integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g== + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== dependencies: - es-abstract "^1.18.0-next.0" - object-inspect "^1.8.0" + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== -sinon@^9.0.1, sinon@^9.2.1: - version "9.2.1" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.2.1.tgz#64cc88beac718557055bd8caa526b34a2231be6d" - integrity sha512-naPfsamB5KEE1aiioaoqJ6MEhdUs/2vtI5w1hPAXX/UwvoPjXcwh1m5HiKx0HGgKR8lQSoFIgY5jM6KK8VrS9w== +sinon@^9.0.1, sinon@^9.2.4: + version "9.2.4" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.2.4.tgz#e55af4d3b174a4443a8762fa8421c2976683752b" + integrity sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg== dependencies: "@sinonjs/commons" "^1.8.1" "@sinonjs/fake-timers" "^6.0.1" - "@sinonjs/formatio" "^5.0.1" - "@sinonjs/samsam" "^5.2.0" + "@sinonjs/samsam" "^5.3.1" diff "^4.0.2" nise "^4.0.4" supports-color "^7.1.0" @@ -8878,15 +8923,6 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== - dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" - slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -8954,9 +8990,9 @@ socks-proxy-agent@^4.0.0: socks "~2.3.2" socks@^2.3.3: - version "2.5.0" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.5.0.tgz#3a7c286db114f67864a4bd8b4207a91d1db3d6db" - integrity sha512-00OqQHp5SCbwm9ecOMJj9aQtMSjwi1uVuGQoxnpKCS50VKZcOZ8z11CTKypmR8sEy7nZimy/qXY7rYJYbRlXmA== + version "2.5.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.5.1.tgz#7720640b6b5ec9a07d556419203baa3f0596df5f" + integrity sha512-oZCsJJxapULAYJaEYBSzMcz8m3jqgGrHaGhkmU/o/PQfFWYWxkAaA0UMGImb6s6tEXfKi959X6VJjMMQ3P6TTQ== dependencies: ip "^1.1.5" smart-buffer "^4.1.0" @@ -9005,9 +9041,9 @@ source-map-support@^0.5.10, source-map-support@^0.5.17, source-map-support@^0.5. source-map "^0.6.0" source-map-url@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" - integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + version "0.4.1" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" + integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== source-map@^0.5.0, source-map@^0.5.6: version "0.5.7" @@ -9070,14 +9106,19 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.6" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" - integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== + version "3.0.7" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz#e9c18a410e5ed7e12442a549fbd8afa767038d65" + integrity sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ== -spdx-license-list@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/spdx-license-list/-/spdx-license-list-6.3.0.tgz#29507bdd88e5f1dcf62634d3c7f9051245e7ef07" - integrity sha512-Qz8ru5VVK5T4cFOBrshIzggzrQ15fVBcpjpZLCVz2j9KNnpslGbw8w1r06v2vi6YP6bnUSY5CXsFCfUypLZ2GA== +spdx-license-list@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/spdx-license-list/-/spdx-license-list-6.4.0.tgz#9850c3699c1d35745285607d064d2a5145049d12" + integrity sha512-4BxgJ1IZxTJuX1YxMGu2cRYK46Bk9zJNTK2/R0wNZR0cm+6SVl26/uG7FQmQtxoJQX1uZ0EpTi2L7zvMLboaBA== + +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" @@ -9093,6 +9134,13 @@ split2@^2.0.0: dependencies: through2 "^2.0.2" +split2@^3.0.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== + dependencies: + readable-stream "^3.0.0" + split@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" @@ -9128,9 +9176,9 @@ ssri@^6.0.0, ssri@^6.0.1: figgy-pudding "^3.5.1" stack-utils@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.3.tgz#db7a475733b5b8bf6521907b18891d29006f7751" - integrity sha512-WldO+YmqhEpjp23eHZRhOT1NQF51STsbxZ+/AdpFD+EhheFxAe5d0WoK4DQVJkSHacPrJJX3OqRAl9CgHf78pg== + version "1.0.4" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.4.tgz#4b600971dcfc6aed0cbdf2a8268177cc916c87c8" + integrity sha512-IPDJfugEGbfizBwBZRZ3xpccMdRyP5lqsBWXGQWimVjua/ccLCeMOAVjlc1R7LxFjo5sEDhyNIXd8mo/AiDS9w== dependencies: escape-string-regexp "^2.0.0" @@ -9141,21 +9189,21 @@ stack-utils@^2.0.2: dependencies: escape-string-regexp "^2.0.0" -standard-version@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/standard-version/-/standard-version-9.0.0.tgz#814055add91eec8679a773768927f927183fc818" - integrity sha512-eRR04IscMP3xW9MJTykwz13HFNYs8jS33AGuDiBKgfo5YrO0qX0Nxb4rjupVwT5HDYL/aR+MBEVLjlmVFmFEDQ== +standard-version@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/standard-version/-/standard-version-9.1.0.tgz#07589469324d967ffe665fa86ef612949a858a80" + integrity sha512-EJcbKUGKBuHjiDSUL5XjPhT1KGVM+UCvv/ti70fHnJwJyJqTSJWl0mWj/Wj0WwsoskyvKWURESzBsZmCCMUZzg== dependencies: chalk "^2.4.2" - conventional-changelog "3.1.23" + conventional-changelog "3.1.24" conventional-changelog-config-spec "2.1.0" - conventional-changelog-conventionalcommits "4.4.0" - conventional-recommended-bump "6.0.10" + conventional-changelog-conventionalcommits "4.5.0" + conventional-recommended-bump "6.0.11" detect-indent "^6.0.0" detect-newline "^3.1.0" dotgitignore "^2.1.0" figures "^3.1.0" - find-up "^4.1.0" + find-up "^5.0.0" fs-access "^1.0.1" git-semver-tags "^4.0.0" semver "^7.1.1" @@ -9202,6 +9250,11 @@ streamroller@^2.2.4: debug "^4.1.1" fs-extra "^8.1.0" +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= + string-length@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.1.tgz#4a973bf31ef77c4edbceadd6af2611996985f8a1" @@ -9250,21 +9303,21 @@ string.prototype.repeat@^0.2.0: resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz#aba36de08dcee6a5a337d49b2ea1da1b28fc0ecf" integrity sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8= -string.prototype.trimend@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz#6ddd9a8796bc714b489a3ae22246a208f37bfa46" - integrity sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw== +string.prototype.trimend@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b" + integrity sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw== dependencies: + call-bind "^1.0.0" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" -string.prototype.trimstart@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz#22d45da81015309cd0cdd79787e8919fc5c613e7" - integrity sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg== +string.prototype.trimstart@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa" + integrity sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg== dependencies: + call-bind "^1.0.0" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" string_decoder@^1.1.1: version "1.3.0" @@ -9417,17 +9470,7 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -table@^5.2.3: - version "5.4.6" - resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== - dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" - -table@^6.0.7: +table@^6.0.4, table@^6.0.7: version "6.0.7" resolved "https://registry.yarnpkg.com/table/-/table-6.0.7.tgz#e45897ffbcc1bcf9e8a87bf420f2c9e5a7a52a34" integrity sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g== @@ -9517,9 +9560,9 @@ tap@^12.0.1: yapool "^1.0.0" tar-stream@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.4.tgz#c4fb1a11eb0da29b893a5b25476397ba2d053bfa" - integrity sha512-o3pS2zlG4gxr67GmFYBLlq+dM8gyRGUOvsrHclSkvtVtQbjV0s/+ZE8OpICbaj8clrX3tjeHngYGP7rweaBnuw== + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== dependencies: bl "^4.0.3" end-of-stream "^1.4.1" @@ -9774,10 +9817,10 @@ trivial-deferred@^1.0.1: resolved "https://registry.yarnpkg.com/trivial-deferred/-/trivial-deferred-1.0.1.tgz#376d4d29d951d6368a6f7a0ae85c2f4d5e0658f3" integrity sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM= -ts-jest@^26.4.4: - version "26.4.4" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.4.tgz#61f13fb21ab400853c532270e52cc0ed7e502c49" - integrity sha512-3lFWKbLxJm34QxyVNNCgXX1u4o/RV0myvA2y2Bxm46iGIjKlaY0own9gIckbjZJPn+WaJEnfPPJ20HHGpoq4yg== +ts-jest@^26.5.0, ts-jest@^26.5.1: + version "26.5.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.1.tgz#4d53ee4481552f57c1624f0bd3425c8b17996150" + integrity sha512-G7Rmo3OJMvlqE79amJX8VJKDiRcd7/r61wh9fnvvG8cAjhA9edklGw/dCxRSQmfZ/z8NDums5srSVgwZos1qfg== dependencies: "@types/jest" "26.x" bs-logger "0.x" @@ -9785,16 +9828,16 @@ ts-jest@^26.4.4: fast-json-stable-stringify "2.x" jest-util "^26.1.0" json5 "2.x" - lodash.memoize "4.x" + lodash "4.x" make-error "1.x" mkdirp "1.x" semver "7.x" yargs-parser "20.x" -ts-mock-imports@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/ts-mock-imports/-/ts-mock-imports-1.3.1.tgz#1bb701c05a24f3ba30621b5e31af123dbd994db8" - integrity sha512-MKEGXb40TUbpQ6b/if424zs0gfTyHfsebw+FUBkqbC0kVoPwoXhoe82lJH4dC92j4vDoId6pSjtIvwvtSMnS5w== +ts-mock-imports@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/ts-mock-imports/-/ts-mock-imports-1.3.3.tgz#7865888382806aa8f09412ab582a8e06f2bd1b6d" + integrity sha512-zCAcb89Y+f3Bhw5VaHrHMh5tiuwAQEl5D3e0r5ELCdLl9EnZpb8Oeei/S60Qf4LORIfmJEXb3TpR5kxtL6j2cg== ts-node@^8.0.2: version "8.10.2" @@ -9807,10 +9850,10 @@ ts-node@^8.0.2: source-map-support "^0.5.17" yn "3.1.1" -ts-node@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.0.tgz#95eae4c6d0f94f2545884078e1eb1b14d2155639" - integrity sha512-0yqcL4sgruCvM+w64LiAfNJo6+lHfCYc5Ajj4yiLNkJ9oZ2HWaa+Kso7htYOOxVQ7+csAjdUjffOe9PIqC4pMg== +ts-node@^9.1.1: + version "9.1.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" + integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== dependencies: arg "^4.1.0" create-require "^1.1.0" @@ -9840,14 +9883,14 @@ tslib@^1.8.1, tslib@^1.9.0: integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== tslib@^2.0.1: - version "2.0.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" - integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== + version "2.1.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== tsutils@^3.17.1: - version "3.17.1" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" - integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + version "3.20.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698" + integrity sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg== dependencies: tslib "^1.8.1" @@ -9887,11 +9930,6 @@ type-fest@^0.11.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== -type-fest@^0.13.1: - version "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.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" @@ -9924,14 +9962,15 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript-json-schema@^0.47.0: - version "0.47.0" - resolved "https://registry.yarnpkg.com/typescript-json-schema/-/typescript-json-schema-0.47.0.tgz#84dde5460b127c6774da81bf70b23c7e04857b13" - integrity sha512-A6NVwSOTSsNDHfaqDcDeKwwyXEeKqBHoAr20jcetnYj4e8C6zVFofAVhAuwsBXCRYiWEE/lyHrcxpsSpbIk0Mg== +typescript-json-schema@^0.49.0: + version "0.49.0" + resolved "https://registry.yarnpkg.com/typescript-json-schema/-/typescript-json-schema-0.49.0.tgz#442f6347ca85fb0d9811f217fb0d6537b68734b3" + integrity sha512-PumZkTmEE3T8TVyoJU6ZCp3K6VCmCb3Ei6fUaRIuDsIzYtmdJc6jV1D0RyBe5sd5mJ1iB6Zckm4KAKbqXs9oDw== dependencies: "@types/json-schema" "^7.0.6" glob "^7.1.6" json-stable-stringify "^1.0.1" + ts-node "^9.1.1" typescript "^4.1.3" yargs "^16.2.0" @@ -9956,9 +9995,9 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== uglify-js@^3.1.4: - version "3.11.6" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.11.6.tgz#144b50d3e05eadd3ad4dd047c60ca541a8cd4e9c" - integrity sha512-oASI1FOJ7BBFkSCNDZ446EgkSuHkOZBuqRFrwXIKWCoXw8ZXQETooTQjkAcBS03Acab7ubCKsXnwuV2svy061g== + version "3.12.7" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.12.7.tgz#be4f06142a67bd91ef868b4e111dc241e151bff3" + integrity sha512-SIZhkoh+U/wjW+BHGhVwE9nt8tWJspncloBcFapkpGRwNPqcH8pzX36BXe3TPBjzHWPMUZotpCigak/udWNr1Q== uid-number@0.0.6: version "0.0.6" @@ -10043,9 +10082,9 @@ upath@^1.2.0: integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== uri-js@^4.2.2: - version "4.4.0" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" - integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" @@ -10105,9 +10144,9 @@ v8-compile-cache@^2.0.3: integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== 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== + version "7.1.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.0.tgz#5b95cef45c0f83217ec79f8fc7ee1c8b486aee07" + integrity sha512-uXUVqNUCLa0AH1vuVxzi+MI4RfxEOKt9pBgKwHbgH7st8Kv2P1m+jvWNnektzBh5QShF3ODgKmUFCf38LnVz1g== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" @@ -10128,6 +10167,11 @@ 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= + verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -10211,15 +10255,15 @@ whatwg-url@^8.0.0: webidl-conversions "^6.1.0" which-boxed-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1" - integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ== + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== dependencies: - is-bigint "^1.0.0" - is-boolean-object "^1.0.0" - is-number-object "^1.0.3" - is-string "^1.0.4" - is-symbol "^1.0.2" + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" which-collection@^1.0.1: version "1.0.1" @@ -10237,12 +10281,13 @@ which-module@^2.0.0: integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= which-typed-array@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2" - integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ== + version "1.1.4" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.4.tgz#8fcb7d3ee5adf2d771066fba7cf37e32fe8711ff" + integrity sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA== dependencies: available-typed-arrays "^1.0.2" - es-abstract "^1.17.5" + call-bind "^1.0.0" + es-abstract "^1.18.0-next.1" foreach "^2.0.5" function-bind "^1.1.1" has-symbols "^1.0.1" @@ -10369,17 +10414,10 @@ write-pkg@^3.1.0: sort-keys "^2.0.0" write-json-file "^2.2.0" -write@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" - ws@^7.2.3: - version "7.4.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.0.tgz#a5dd76a24197940d4a8bb9e0e152bb4503764da7" - integrity sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ== + version "7.4.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd" + integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA== xml-js@^1.6.11: version "1.6.11" @@ -10437,9 +10475,9 @@ xtend@~4.0.1: integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + version "4.0.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" + integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== y18n@^5.0.5: version "5.0.5" @@ -10497,7 +10535,7 @@ yargs-parser@^15.0.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^18.1.2, yargs-parser@^18.1.3: +yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== @@ -10555,7 +10593,7 @@ yargs@^15.0.2, yargs@^15.3.1, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.0.3, yargs@^16.1.1, yargs@^16.2.0: +yargs@^16.0.3, yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== @@ -10573,6 +10611,11 @@ yn@3.1.1: resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + zip-stream@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.0.4.tgz#3a8f100b73afaa7d1ae9338d910b321dec77ff3a"