diff --git a/.gitallowed b/.gitallowed index 972728e4b4a7e..2fa8726e1171d 100644 --- a/.gitallowed +++ b/.gitallowed @@ -22,3 +22,4 @@ account: '772975370895' account: '856666278305' account: '840364872350' account: '422531588944' +account: '924023996002' diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml index cf2ed7e21ca69..c289f5381995c 100644 --- a/.github/workflows/auto-approve.yml +++ b/.github/workflows/auto-approve.yml @@ -1,7 +1,9 @@ # Approve PRs with "pr/auto-approve". mergify takes care of the actual merge. name: auto-approve -on: pull_request +on: + pull_request: + types: [ labeled, unlabeled, opened, synchronize, reopened, ready_for_review, review_requested ] jobs: auto-approve: diff --git a/.github/workflows/pr-linter.yml b/.github/workflows/pr-linter.yml index bb8c6dd66943b..5702b254d4a0b 100644 --- a/.github/workflows/pr-linter.yml +++ b/.github/workflows/pr-linter.yml @@ -2,7 +2,15 @@ # https://github.com/actions/toolkit/blob/master/packages/github/src/context.ts name: PR Linter -on: pull_request +on: + pull_request: + types: + - labeled + - unlabeled + - edited + - opened + - synchronize + - reopened jobs: validate-pr: diff --git a/.github/workflows/yarn-upgrade.yml b/.github/workflows/yarn-upgrade.yml index 17617a75537d3..91daf74640bb8 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.4 + uses: actions/setup-node@v2.1.5 with: node-version: 10 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dd8c2c1991b1..fad4611dfad6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,88 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.92.0](https://github.com/aws/aws-cdk/compare/v1.91.0...v1.92.0) (2021-03-06) + +* **ecs-patterns**: the `desiredCount` property stored on the above constructs will be optional, allowing them to be undefined. This is enabled through the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` feature flag. We would recommend all CDK users to set the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` flag to `true` for all of their existing applications. + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **apigatewayv2:** `HttpApiMapping` (and related interfaces for `Attributed` and `Props`) has been renamed to `ApiMapping` +* **apigatewayv2:** `CommonStageOptions` has been renamed to `StageOptions` +* **apigatewayv2:** `HttpStage.fromStageName` has been removed in favour of `HttpStage.fromHttpStageAttributes` +* **apigatewayv2:** `DefaultDomainMappingOptions` has been removed in favour of `DomainMappingOptions` +* **apigatewayv2:** `HttpApiProps.defaultDomainMapping` has been changed from `DefaultDomainMappingOptions` to `DomainMappingOptions` +* **apigatewayv2:** `HttpApi.defaultStage` has been changed from `HttpStage` to `IStage` +* **apigatewayv2:** `IHttpApi.defaultStage` has been removed +* **aws-appsync:** RdsDataSource now takes a ServerlessCluster instead of a DatabaseCluster +* **aws-appsync:** graphqlapi.addRdsDataSource now takes databaseName as its fourth argument + +### Features + +* **apigateway:** integrate with aws services in a different region ([#13251](https://github.com/aws/aws-cdk/issues/13251)) ([d942699](https://github.com/aws/aws-cdk/commit/d9426996c07ff909993594ed91cfcf2b5761414b)), closes [#7009](https://github.com/aws/aws-cdk/issues/7009) +* **apigatewayv2:** websocket api ([#13031](https://github.com/aws/aws-cdk/issues/13031)) ([fe1c839](https://github.com/aws/aws-cdk/commit/fe1c8393e0840fb273c4a5f325cb3cebc784bf4b)), closes [#2872](https://github.com/aws/aws-cdk/issues/2872) +* **aws-appsync:** add databaseName to rdsDataSource ([#12575](https://github.com/aws/aws-cdk/issues/12575)) ([f92b65e](https://github.com/aws/aws-cdk/commit/f92b65e2a158f918d8f05132ed12a4bb85228997)), closes [#12572](https://github.com/aws/aws-cdk/issues/12572) +* **aws-events:** Event Bus target ([#12926](https://github.com/aws/aws-cdk/issues/12926)) ([ea91aa3](https://github.com/aws/aws-cdk/commit/ea91aa31db9e2f31c734ad6d7e1f64d5d432dfd4)), closes [#9473](https://github.com/aws/aws-cdk/issues/9473) +* **aws-route53-targets:** add global accelerator target to route53 alias targets ([#13407](https://github.com/aws/aws-cdk/issues/13407)) ([2672a55](https://github.com/aws/aws-cdk/commit/2672a55c393e5ce7dd9a230d921ec1be1a23e32a)), closes [#12839](https://github.com/aws/aws-cdk/issues/12839) +* **aws-s3:** adds s3 bucket AWS FSBP option ([#12804](https://github.com/aws/aws-cdk/issues/12804)) ([b9cdd52](https://github.com/aws/aws-cdk/commit/b9cdd52274eca55940c65b830939132d0e074365)), closes [#10969](https://github.com/aws/aws-cdk/issues/10969) +* **cfnspec:** cloudformation spec v28.0.0 ([#13101](https://github.com/aws/aws-cdk/issues/13101)) ([13c9859](https://github.com/aws/aws-cdk/commit/13c9859cc62b3d472ba1be84b12d478f61f02ec9)) +* **cfnspec:** cloudformation spec v29.0.0 ([#13249](https://github.com/aws/aws-cdk/issues/13249)) ([6318e26](https://github.com/aws/aws-cdk/commit/6318e2632297783bc8b5b2609bba096dd83a1113)) +* **cfnspec:** cloudformation spec v30.0.0 ([#13365](https://github.com/aws/aws-cdk/issues/13365)) ([ae0185d](https://github.com/aws/aws-cdk/commit/ae0185dd089e3bb7c5639ebc1bce3f95e126f71c)) +* **cli:** Configurable --change-set-name CLI flag ([#13024](https://github.com/aws/aws-cdk/issues/13024)) ([18184df](https://github.com/aws/aws-cdk/commit/18184df05f5b8478ef9cae1285e45e61a0833822)), closes [#11075](https://github.com/aws/aws-cdk/issues/11075) [#12683](https://github.com/aws/aws-cdk/pull/12683) +* **cloudwatch:** EC2 actions ([#13281](https://github.com/aws/aws-cdk/issues/13281)) ([319cfcd](https://github.com/aws/aws-cdk/commit/319cfcdaaf92e4e6edb8c2388d04dce0971aaf86)), closes [#13228](https://github.com/aws/aws-cdk/issues/13228) +* **cognito:** user pools - sign in with apple ([#13160](https://github.com/aws/aws-cdk/issues/13160)) ([b965589](https://github.com/aws/aws-cdk/commit/b965589358f4c281aea36404276f08128e6ff3db)) +* **core:** `description` parameter in the CustomResourceProvider ([#13275](https://github.com/aws/aws-cdk/issues/13275)) ([78831cf](https://github.com/aws/aws-cdk/commit/78831cf9dec0407e7d827711183ac47be070f480)), closes [#13277](https://github.com/aws/aws-cdk/issues/13277) [#13276](https://github.com/aws/aws-cdk/issues/13276) +* **core:** customize bundling output packaging ([#13152](https://github.com/aws/aws-cdk/issues/13152)) ([6eca979](https://github.com/aws/aws-cdk/commit/6eca979f65542f3e44461588d8220e8c0bf76a6e)) +* **ec2:** Add VPC endpoint for RDS ([#12497](https://github.com/aws/aws-cdk/issues/12497)) ([fc87574](https://github.com/aws/aws-cdk/commit/fc8757437c37a0947cced720ff363b8858850f72)), closes [#12402](https://github.com/aws/aws-cdk/issues/12402) +* **ecs:** add port mappings to containers with props ([#13262](https://github.com/aws/aws-cdk/issues/13262)) ([f511639](https://github.com/aws/aws-cdk/commit/f511639bba156f6edd15896a4dd8e27b07671ea1)), closes [#13261](https://github.com/aws/aws-cdk/issues/13261) +* **ecs:** allow selection of container and port for SRV service discovery records ([#12798](https://github.com/aws/aws-cdk/issues/12798)) ([a452bc3](https://github.com/aws/aws-cdk/commit/a452bc385640762a043392a717d49de29abcc64e)), closes [#12796](https://github.com/aws/aws-cdk/issues/12796) +* **ecs-patterns:** Add support for assignPublicIp for QueueProcessingFargateService ([#13122](https://github.com/aws/aws-cdk/issues/13122)) ([3fb4600](https://github.com/aws/aws-cdk/commit/3fb46001a7345cbefa6df70893999bcb304ed40d)), closes [#12815](https://github.com/aws/aws-cdk/issues/12815) +* **ecs-patterns:** remove default desiredCount to align with cfn behaviour (under feature flag) ([#13130](https://github.com/aws/aws-cdk/issues/13130)) ([a9caa45](https://github.com/aws/aws-cdk/commit/a9caa455b708e08f1cf2d366ac32892d4faa59b4)) +* **elasticloadbalancingv2:** Add support for application cookies ([#13142](https://github.com/aws/aws-cdk/issues/13142)) ([23385dd](https://github.com/aws/aws-cdk/commit/23385ddeb0decd227a0104d7b0aff06939acaad9)) +* **elbv2:** allow control of ingress rules on redirect listener ([#12768](https://github.com/aws/aws-cdk/issues/12768)) ([b7b441f](https://github.com/aws/aws-cdk/commit/b7b441f74a07d26fd8de23df84e7ab4663c89c0c)), closes [#12766](https://github.com/aws/aws-cdk/issues/12766) +* **events:** archive events ([#12060](https://github.com/aws/aws-cdk/issues/12060)) ([465cd9c](https://github.com/aws/aws-cdk/commit/465cd9c434acff74070ca6d33891e1481e253128)), closes [#11531](https://github.com/aws/aws-cdk/issues/11531) +* **events:** dead letter queue for Lambda Targets ([#11617](https://github.com/aws/aws-cdk/issues/11617)) ([1bb3650](https://github.com/aws/aws-cdk/commit/1bb3650c5dd2087b05793a5e903cdfb80fc5c1ad)), closes [#11612](https://github.com/aws/aws-cdk/issues/11612) +* **lambda:** code signing config ([#12656](https://github.com/aws/aws-cdk/issues/12656)) ([778ea27](https://github.com/aws/aws-cdk/commit/778ea2759a8a4504dc232eb6b1d77a38f8ee7aef)), closes [#12216](https://github.com/aws/aws-cdk/issues/12216) +* **lambda:** Code.fromDockerBuild ([#13318](https://github.com/aws/aws-cdk/issues/13318)) ([ad01099](https://github.com/aws/aws-cdk/commit/ad01099d5b8f835c3b87d7d20fd2dc1a5df2fd6f)), closes [#13273](https://github.com/aws/aws-cdk/issues/13273) +* **lambda:** Code.fromDockerBuildAsset ([#12258](https://github.com/aws/aws-cdk/issues/12258)) ([09afed5](https://github.com/aws/aws-cdk/commit/09afed5ca2b39919c1c84d200370d490110cd0d1)), closes [#11914](https://github.com/aws/aws-cdk/issues/11914) +* **neptune:** high level constructs for db clusters and instances ([#12763](https://github.com/aws/aws-cdk/issues/12763)) ([c366837](https://github.com/aws/aws-cdk/commit/c36683701d88eb0c53fdd2add66b10c47c05f56b)), closes [aws#12762](https://github.com/aws/aws/issues/12762) +* **stepfunctions-tasks:** add EKS call to SFN-tasks ([#12779](https://github.com/aws/aws-cdk/issues/12779)) ([296a10d](https://github.com/aws/aws-cdk/commit/296a10d76a9f6fc2a374d1a6461c460bcc3eeb79)) +* **synthetics:** Update CloudWatch Synthetics NodeJS runtimes ([#12907](https://github.com/aws/aws-cdk/issues/12907)) ([6aac3b6](https://github.com/aws/aws-cdk/commit/6aac3b6a9bb1586ee16e7a85ca657b544d0f8304)), closes [#12906](https://github.com/aws/aws-cdk/issues/12906) + + +### Bug Fixes + +* **appsync:** revert to allow resolver creation from data source ([#12973](https://github.com/aws/aws-cdk/issues/12973)) ([d35f032](https://github.com/aws/aws-cdk/commit/d35f03226d6d7fb5be246b4d3584ee9205b0ef2d)), closes [#12635](https://github.com/aws/aws-cdk/issues/12635) [#11522](https://github.com/aws/aws-cdk/issues/11522) +* **aws-appsync:** use serverlessCluster on rdsDataSource ([#13206](https://github.com/aws/aws-cdk/issues/13206)) ([45cf387](https://github.com/aws/aws-cdk/commit/45cf3873fb48d4043e7a22284d36695ea6bde6ef)), closes [#12567](https://github.com/aws/aws-cdk/issues/12567) +* **cfn-diff:** handle Fn::If inside policies and statements ([#12975](https://github.com/aws/aws-cdk/issues/12975)) ([daf4e47](https://github.com/aws/aws-cdk/commit/daf4e47a790ab99639e471f6792f22e3e4f8ee73)), closes [#12887](https://github.com/aws/aws-cdk/issues/12887) +* **cfn-include:** allow dynamic mappings to be used in Fn::FindInMap ([#13428](https://github.com/aws/aws-cdk/issues/13428)) ([623675d](https://github.com/aws/aws-cdk/commit/623675d2f8fb2786f23beb87994e687e8a7c6612)) +* **cloudfront:** cannot add two EdgeFunctions with same aliases ([#13324](https://github.com/aws/aws-cdk/issues/13324)) ([1f35351](https://github.com/aws/aws-cdk/commit/1f3535145d22b2b13ebbcbfe31a3bfd73519352d)), closes [#13237](https://github.com/aws/aws-cdk/issues/13237) +* **cloudwatch:** MathExpression period of <5 minutes is not respected ([#13078](https://github.com/aws/aws-cdk/issues/13078)) ([d9ee914](https://github.com/aws/aws-cdk/commit/d9ee91432918aa113f728abdd61295096ed1512f)), closes [#9156](https://github.com/aws/aws-cdk/issues/9156) +* **cloudwatch:** metric `label` not rendered into Alarms ([#13070](https://github.com/aws/aws-cdk/issues/13070)) ([cbcc712](https://github.com/aws/aws-cdk/commit/cbcc712e0c4c44c83c7f4d1e8a544bccfa26bb56)) +* **codebuild:** allow FILE_PATH webhook filter for BitBucket ([#13186](https://github.com/aws/aws-cdk/issues/13186)) ([cbed348](https://github.com/aws/aws-cdk/commit/cbed3488f03bdfba16f3950bda653535c8999db1)), closes [#13175](https://github.com/aws/aws-cdk/issues/13175) +* **core:** custom resource provider NODEJS_12 now looks like Lambda's NODEJS_12_X, add Node 14 ([#13301](https://github.com/aws/aws-cdk/issues/13301)) ([3413b2f](https://github.com/aws/aws-cdk/commit/3413b2f887596d11dfb53c0e99c2a1788095a2ad)) +* **core:** ENOTDIR invalid cwd on "cdk deploy" ([#13145](https://github.com/aws/aws-cdk/issues/13145)) ([cd7a3ed](https://github.com/aws/aws-cdk/commit/cd7a3ed333570a3b26446e1e3a054ca886cd3906)), closes [#12258](https://github.com/aws/aws-cdk/issues/12258) [#13076](https://github.com/aws/aws-cdk/issues/13076) [#13131](https://github.com/aws/aws-cdk/issues/13131) +* **custom-resources:** unable to use a resource attributes as dictionary keys in AwsCustomResource ([#13074](https://github.com/aws/aws-cdk/issues/13074)) ([3cb3104](https://github.com/aws/aws-cdk/commit/3cb31043a42b035f6dcd2a318836d4bfc4973151)), closes [#13063](https://github.com/aws/aws-cdk/issues/13063) +* **dynamodb:** replicas not created on table replacement ([#13300](https://github.com/aws/aws-cdk/issues/13300)) ([c7c424f](https://github.com/aws/aws-cdk/commit/c7c424fec42f1f14ab8bdc3011f5bdb602918aa3)), closes [#12332](https://github.com/aws/aws-cdk/issues/12332) +* **ec2:** NAT provider's default outbound rules cannot be disabled ([#12674](https://github.com/aws/aws-cdk/issues/12674)) ([664133a](https://github.com/aws/aws-cdk/commit/664133a35da2bd096a237971ce662f3dd38b297f)), closes [#12673](https://github.com/aws/aws-cdk/issues/12673) +* **ec2:** readme grammar ([#13180](https://github.com/aws/aws-cdk/issues/13180)) ([fe4f056](https://github.com/aws/aws-cdk/commit/fe4f05678c06d634d3fe9e1b608e444a57f67b9c)) +* **ec2:** Throw error on empty InitFile content ([#13009](https://github.com/aws/aws-cdk/issues/13009)) ([#13119](https://github.com/aws/aws-cdk/issues/13119)) ([81a78a3](https://github.com/aws/aws-cdk/commit/81a78a31408276ebb020e45b15ddca7a2c57ae50)) +* **ecr:** Allow referencing an EcrImage by digest instead of tag ([#13299](https://github.com/aws/aws-cdk/issues/13299)) ([266a621](https://github.com/aws/aws-cdk/commit/266a621abfc34c62ff1e26de9cb8cf0687588f89)), closes [#5082](https://github.com/aws/aws-cdk/issues/5082) +* **ecr:** Generate valid CloudFormation for imageScanOnPush ([#13420](https://github.com/aws/aws-cdk/issues/13420)) ([278fba5](https://github.com/aws/aws-cdk/commit/278fba5df4a3d785e49bdb57ccf88fd34bacacbb)), closes [#13418](https://github.com/aws/aws-cdk/issues/13418) +* **ecs:** services essential container exceptions thrown too soon ([#13240](https://github.com/aws/aws-cdk/issues/13240)) ([c174f6c](https://github.com/aws/aws-cdk/commit/c174f6c2f4dd909e07be34b66bd6b3a92d5e8484)), closes [#13239](https://github.com/aws/aws-cdk/issues/13239) +* **eks:** `KubectlProvider` creates un-necessary security group ([#13178](https://github.com/aws/aws-cdk/issues/13178)) ([c5e8b6d](https://github.com/aws/aws-cdk/commit/c5e8b6df1e5f0359d51d025edcc68508ab5daef1)) +* UserPool, Volume, ElasticSearch, FSx are now RETAIN by default ([#12920](https://github.com/aws/aws-cdk/issues/12920)) ([5a54741](https://github.com/aws/aws-cdk/commit/5a54741a414d3f8b7913163f4785759b984b41d8)), closes [#12563](https://github.com/aws/aws-cdk/issues/12563) +* **eks:** Deployment fails for the first deployment in an account ([#13103](https://github.com/aws/aws-cdk/issues/13103)) ([e042879](https://github.com/aws/aws-cdk/commit/e042879851f8ddd558d20941019c9a6692a1c2bf)), closes [#9027](https://github.com/aws/aws-cdk/issues/9027) +* incorrect peerDependency on "constructs" ([#13255](https://github.com/aws/aws-cdk/issues/13255)) ([17244af](https://github.com/aws/aws-cdk/commit/17244af0d181a28b908fa161250c5a3285521c53)) +* **elasticloadbalancingv2:** should allow more than 2 certificates ([#13332](https://github.com/aws/aws-cdk/issues/13332)) ([d3155e9](https://github.com/aws/aws-cdk/commit/d3155e97fd9331a4732396941ce4ad20613fe81c)), closes [#13150](https://github.com/aws/aws-cdk/issues/13150) +* **events:** cannot trigger multiple Lambdas from the same Rule ([#13260](https://github.com/aws/aws-cdk/issues/13260)) ([c8c1762](https://github.com/aws/aws-cdk/commit/c8c1762c213aad1062c3a0bc48b22b05c3a0a185)), closes [#13231](https://github.com/aws/aws-cdk/issues/13231) +* **events:** imported ECS Task Definition cannot be used as target ([#13293](https://github.com/aws/aws-cdk/issues/13293)) ([6f7cebd](https://github.com/aws/aws-cdk/commit/6f7cebdf61073cc1fb358fcac5f5b2156389cb81)), closes [#12811](https://github.com/aws/aws-cdk/issues/12811) +* **lambda-nodejs:** 'must use "outdir"' error with spaces in paths ([#13268](https://github.com/aws/aws-cdk/issues/13268)) ([09723f5](https://github.com/aws/aws-cdk/commit/09723f58ed3034fc2cb46316e6d798cb8f2bf96e)), closes [#13210](https://github.com/aws/aws-cdk/issues/13210) +* **lambda-nodejs:** invalid sample in documentation ([#12404](https://github.com/aws/aws-cdk/issues/12404)) ([520c263](https://github.com/aws/aws-cdk/commit/520c263ca3c6b0ea7d9c09c23e509a3373ee2b8a)) +* **lambda-nodejs:** paths with spaces break esbuild ([#13312](https://github.com/aws/aws-cdk/issues/13312)) ([f983fbb](https://github.com/aws/aws-cdk/commit/f983fbb474ecd6727b0c5a35333718cc55d78bf1)), closes [#13311](https://github.com/aws/aws-cdk/issues/13311) +* **lambda-python:** asset hash is non-deterministic ([#12984](https://github.com/aws/aws-cdk/issues/12984)) ([37debc0](https://github.com/aws/aws-cdk/commit/37debc0513c5174ca3d918fce94a138d5d34b586)), closes [#12770](https://github.com/aws/aws-cdk/issues/12770) [#12684](https://github.com/aws/aws-cdk/issues/12684) +* **stepfunctions:** `SageMakeUpdateEndpoint` adds insufficient permissions ([#13170](https://github.com/aws/aws-cdk/issues/13170)) ([6126e49](https://github.com/aws/aws-cdk/commit/6126e499e5ca22b5f751af4f4f05d74f696829f1)), closes [#11594](https://github.com/aws/aws-cdk/issues/11594) + ## [1.91.0](https://github.com/aws/aws-cdk/compare/v1.90.1...v1.91.0) (2021-02-23) diff --git a/pack.sh b/pack.sh index 727c6faaf63cd..b85c92f3e68a5 100755 --- a/pack.sh +++ b/pack.sh @@ -63,6 +63,16 @@ for dir in $(find packages -name dist | grep -v node_modules | grep -v run-wrapp rsync -a $dir/ ${distdir}/ done +# Record the dependency order of NPM packages into a file +# (This file will be opportunistically used during publishing) +# +# Manually sort 'aws-cdk' to the end, as the 'cdk init' command has implicit dependencies +# on other packages (that should not appear in 'package.json' and so +# there is no way to tell lerna about these). +for dir in $(lerna ls --toposort -p | grep -v packages/aws-cdk) $PWD/packages/aws-cdk; do + (cd $dir/dist/js && ls >> ${distdir}/js/npm-publish-order.txt) || true +done + # Remove a JSII aggregate POM that may have snuk past rm -rf dist/java/software/amazon/jsii diff --git a/package.json b/package.json index 84eeb77a0016c..6ddae6c7ad5d5 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,9 @@ "fs-extra": "^9.1.0", "graceful-fs": "^4.2.6", "jest-junit": "^12.0.0", - "jsii-diff": "^1.23.0", - "jsii-pacmak": "^1.23.0", - "jsii-rosetta": "^1.23.0", + "jsii-diff": "^1.24.0", + "jsii-pacmak": "^1.24.0", + "jsii-rosetta": "^1.24.0", "lerna": "^3.22.1", "standard-version": "^9.1.1", "typescript": "~3.9.9" 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 35436d11ce691..7749683fb4235 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 @@ -165,6 +165,7 @@ export class AppMeshExtension extends ServiceExtension { 'me-south-1': this.accountIdForRegion('me-south-1'), 'ap-east-1': this.accountIdForRegion('ap-east-1'), + 'af-south-1': this.accountIdForRegion('af-south-1'), }, }); diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json index 7ef708ccc31aa..a6e83c8b6ad66 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json @@ -3354,6 +3354,9 @@ }, "ap-east-1": { "ecrRepo": "856666278305" + }, + "af-south-1": { + "ecrRepo": "924023996002" } }, "greetingenvoyimageaccountmapping": { @@ -3413,6 +3416,9 @@ }, "ap-east-1": { "ecrRepo": "856666278305" + }, + "af-south-1": { + "ecrRepo": "924023996002" } }, "greeterenvoyimageaccountmapping": { @@ -3472,6 +3478,9 @@ }, "ap-east-1": { "ecrRepo": "856666278305" + }, + "af-south-1": { + "ecrRepo": "924023996002" } } }, diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json index 2aab9da2612fa..85c6a59e919de 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json @@ -2173,6 +2173,9 @@ }, "ap-east-1": { "ecrRepo": "856666278305" + }, + "af-south-1": { + "ecrRepo": "924023996002" } }, "namedevelopmentenvoyimageaccountmapping": { @@ -2232,6 +2235,9 @@ }, "ap-east-1": { "ecrRepo": "856666278305" + }, + "af-south-1": { + "ecrRepo": "924023996002" } } } diff --git a/packages/@aws-cdk/assert/package.json b/packages/@aws-cdk/assert/package.json index 17a2d01fae5c9..95d88e937b9d0 100644 --- a/packages/@aws-cdk/assert/package.json +++ b/packages/@aws-cdk/assert/package.json @@ -25,7 +25,7 @@ "cdk-build-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.5.2" + "ts-jest": "^26.5.3" }, "dependencies": { "@aws-cdk/cloud-assembly-schema": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md index 6dd9de9e4e475..cce77fd6398e6 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md @@ -21,6 +21,8 @@ - [Lambda Integration](#lambda) - [HTTP Proxy Integration](#http-proxy) - [Private Integration](#private-integration) +- [WebSocket APIs](#websocket-apis) + - [Lambda WebSocket Integration](#lambda-websocket-integration) ## HTTP APIs @@ -146,3 +148,32 @@ const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { }), }); ``` + +## WebSocket APIs + +WebSocket integrations connect a route to backend resources. The following integrations are supported in the CDK. + +### Lambda WebSocket Integration + +Lambda integrations enable integrating a WebSocket API route with a Lambda function. When a client connects/disconnects +or sends message specific to a route, the API Gateway service forwards the request to the Lambda function + +The API Gateway service will invoke the lambda function with an event payload of a specific format. + +The following code configures a `sendmessage` route with a Lambda integration + +```ts +const webSocketApi = new WebSocketApi(stack, 'mywsapi'); +new WebSocketStage(stack, 'mystage', { + webSocketApi, + stageName: 'dev', + autoDeploy: true, +}); + +const messageHandler = new lambda.Function(stack, 'MessageHandler', {...}); +webSocketApi.addRoute('sendmessage', { + integration: new LambdaWebSocketIntegration({ + handler: connectHandler, + }), +}); +``` diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts index a962b268d7165..220d3dca57210 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/lambda.ts @@ -41,7 +41,7 @@ export class LambdaProxyIntegration implements IHttpRouteIntegration { principal: new ServicePrincipal('apigateway.amazonaws.com'), sourceArn: Stack.of(route).formatArn({ service: 'execute-api', - resource: route.httpApi.httpApiId, + resource: route.httpApi.apiId, resourceName: `*/*${route.path ?? ''}`, // empty string in the case of the catch-all route $default }), }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts index c202386ae710e..fd16aff655ff2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts @@ -1 +1,2 @@ export * from './http'; +export * from './websocket'; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts new file mode 100644 index 0000000000000..04a64da0c7540 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/index.ts @@ -0,0 +1 @@ +export * from './lambda'; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts new file mode 100644 index 0000000000000..85e199a71c3d7 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/websocket/lambda.ts @@ -0,0 +1,44 @@ +import { + IWebSocketRouteIntegration, + WebSocketIntegrationType, + WebSocketRouteIntegrationBindOptions, + WebSocketRouteIntegrationConfig, +} from '@aws-cdk/aws-apigatewayv2'; +import { ServicePrincipal } from '@aws-cdk/aws-iam'; +import { IFunction } from '@aws-cdk/aws-lambda'; +import { Names, Stack } from '@aws-cdk/core'; + +/** + * Lambda WebSocket Integration props + */ +export interface LambdaWebSocketIntegrationProps { + /** + * The handler for this integration. + */ + readonly handler: IFunction +} + +/** + * Lambda WebSocket Integration + */ +export class LambdaWebSocketIntegration implements IWebSocketRouteIntegration { + constructor(private props: LambdaWebSocketIntegrationProps) {} + + bind(options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + const route = options.route; + this.props.handler.addPermission(`${Names.nodeUniqueId(route.node)}-Permission`, { + scope: options.scope, + principal: new ServicePrincipal('apigateway.amazonaws.com'), + sourceArn: Stack.of(route).formatArn({ + service: 'execute-api', + resource: route.webSocketApi.apiId, + resourceName: `*/*${route.routeKey}`, + }), + }); + + return { + type: WebSocketIntegrationType.AWS_PROXY, + uri: this.props.handler.functionArn, + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json new file mode 100644 index 0000000000000..48bf164ada435 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.expected.json @@ -0,0 +1,534 @@ +{ + "Resources": { + "ConnectHandlerServiceRole7E4A9B1F": { + "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" + ] + ] + } + ] + } + }, + "ConnectHandler2FFD52D8": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"connected\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "ConnectHandlerServiceRole7E4A9B1F", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "ConnectHandlerServiceRole7E4A9B1F" + ] + }, + "DisconnectHandlerServiceRoleE54F14F9": { + "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" + ] + ] + } + ] + } + }, + "DisconnectHandlerCB7ED6F7": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"disconnected\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "DisconnectHandlerServiceRoleE54F14F9", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "DisconnectHandlerServiceRoleE54F14F9" + ] + }, + "DefaultHandlerServiceRoleDF00569C": { + "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" + ] + ] + } + ] + } + }, + "DefaultHandler604DF7AC": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"default\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "DefaultHandlerServiceRoleDF00569C", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "DefaultHandlerServiceRoleDF00569C" + ] + }, + "MessageHandlerServiceRoleDF05266A": { + "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" + ] + ] + } + ] + } + }, + "MessageHandlerDFBBCD6B": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: \"received\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "MessageHandlerServiceRoleDF05266A", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "MessageHandlerServiceRoleDF05266A" + ] + }, + "mywsapi32E6CE11": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "mywsapi", + "ProtocolType": "WEBSOCKET", + "RouteSelectionExpression": "$request.body.action" + } + }, + "mywsapiconnectRouteWebSocketApiIntegmywsapiconnectRoute456CB290Permission2D0BC294": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ConnectHandler2FFD52D8", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "mywsapi32E6CE11" + }, + "/*/*$connect" + ] + ] + } + } + }, + "mywsapiconnectRouteWebSocketIntegration50b017444a02be00a0b575d123314581176017EE": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "ConnectHandler2FFD52D8", + "Arn" + ] + } + } + }, + "mywsapiconnectRoute45A0ED6A": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteKey": "$connect", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapiconnectRouteWebSocketIntegration50b017444a02be00a0b575d123314581176017EE" + } + ] + ] + } + } + }, + "mywsapidisconnectRouteWebSocketApiIntegmywsapidisconnectRoute26B84CF3PermissionB3F6D0A8": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "DisconnectHandlerCB7ED6F7", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "mywsapi32E6CE11" + }, + "/*/*$disconnect" + ] + ] + } + } + }, + "mywsapidisconnectRouteWebSocketIntegrationcd3bacb451e82549501e141cc094d7ba1F7F68BC": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "DisconnectHandlerCB7ED6F7", + "Arn" + ] + } + } + }, + "mywsapidisconnectRoute421A8CB9": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteKey": "$disconnect", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapidisconnectRouteWebSocketIntegrationcd3bacb451e82549501e141cc094d7ba1F7F68BC" + } + ] + ] + } + } + }, + "mywsapidefaultRouteWebSocketApiIntegmywsapidefaultRouteA13D926BPermission58B64FCE": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "DefaultHandler604DF7AC", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "mywsapi32E6CE11" + }, + "/*/*$default" + ] + ] + } + } + }, + "mywsapidefaultRouteWebSocketIntegration640ac0772c157aa8b9a56aa99adbd9d7A2B7F2FA": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "DefaultHandler604DF7AC", + "Arn" + ] + } + } + }, + "mywsapidefaultRouteE9382DF8": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapidefaultRouteWebSocketIntegration640ac0772c157aa8b9a56aa99adbd9d7A2B7F2FA" + } + ] + ] + } + } + }, + "mywsapisendmessageRouteWebSocketApiIntegmywsapisendmessageRoute8A775F3CPermission660FB575": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MessageHandlerDFBBCD6B", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "mywsapi32E6CE11" + }, + "/*/*sendmessage" + ] + ] + } + } + }, + "mywsapisendmessageRouteWebSocketIntegrationcf58a195e318f43f52c4d9ac6d6d2430786B6471": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "MessageHandlerDFBBCD6B", + "Arn" + ] + } + } + }, + "mywsapisendmessageRouteAE873328": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "RouteKey": "sendmessage", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "mywsapisendmessageRouteWebSocketIntegrationcf58a195e318f43f52c4d9ac6d6d2430786B6471" + } + ] + ] + } + } + }, + "mystage114C35EC": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "mywsapi32E6CE11" + }, + "StageName": "dev", + "AutoDeploy": true + } + } + }, + "Outputs": { + "ApiEndpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "wss://", + { + "Ref": "mywsapi32E6CE11" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/dev" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts new file mode 100644 index 0000000000000..01e25f906b0f8 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/integ.lambda.ts @@ -0,0 +1,54 @@ +import { WebSocketApi, WebSocketStage } from '@aws-cdk/aws-apigatewayv2'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { LambdaWebSocketIntegration } from '../../lib'; + +/* + * Stack verification steps: + * 1. Connect: 'wscat -c '. Should connect successfully and print event data containing connectionId in cloudwatch + * 2. SendMessage: '> {"action": "sendmessage", "data": "some-data"}'. Should send the message successfully and print the data in cloudwatch + * 3. Default: '> {"data": "some-data"}'. Should send the message successfully and print the data in cloudwatch + * 4. Disconnect: disconnect from the wscat. Should print event data containing connectionId in cloudwatch + */ + +const app = new App(); +const stack = new Stack(app, 'WebSocketApiInteg'); + +const connectHandler = new lambda.Function(stack, 'ConnectHandler', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "connected" }; };'), +}); + +const disconnetHandler = new lambda.Function(stack, 'DisconnectHandler', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "disconnected" }; };'), +}); + +const defaultHandler = new lambda.Function(stack, 'DefaultHandler', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "default" }; };'), +}); + +const messageHandler = new lambda.Function(stack, 'MessageHandler', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { console.log(event); return { statusCode: 200, body: "received" }; };'), +}); + +const webSocketApi = new WebSocketApi(stack, 'mywsapi', { + connectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: connectHandler }) }, + disconnectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: disconnetHandler }) }, + defaultRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: defaultHandler }) }, +}); +const stage = new WebSocketStage(stack, 'mystage', { + webSocketApi, + stageName: 'dev', + autoDeploy: true, +}); + +webSocketApi.addRoute('sendmessage', { integration: new LambdaWebSocketIntegration({ handler: messageHandler }) }); + +new CfnOutput(stack, 'ApiEndpoint', { value: stage.url }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts new file mode 100644 index 0000000000000..5f431ca28fc49 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/websocket/lambda.test.ts @@ -0,0 +1,35 @@ +import '@aws-cdk/assert/jest'; +import { WebSocketApi } from '@aws-cdk/aws-apigatewayv2'; +import { Code, Function, Runtime } from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import { LambdaWebSocketIntegration } from '../../lib'; + + +describe('LambdaWebSocketIntegration', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const fooFn = fooFunction(stack, 'Fn'); + + // WHEN + new WebSocketApi(stack, 'Api', { + connectRouteOptions: { + integration: new LambdaWebSocketIntegration({ handler: fooFn }), + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'AWS_PROXY', + IntegrationUri: stack.resolve(fooFn.functionArn), + }); + }); +}); + +function fooFunction(stack: Stack, id: string) { + return new Function(stack, id, { + code: Code.fromInline('foo'), + runtime: Runtime.NODEJS_12_X, + handler: 'index.handler', + }); +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 4da900f271e8f..d8278a800a00f 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -7,7 +7,7 @@ Features | Stability -------------------------------------------|-------------------------------------------------------- CFN Resources | ![Stable](https://img.shields.io/badge/stable-success.svg?style=for-the-badge) Higher level constructs for HTTP APIs | ![Experimental](https://img.shields.io/badge/experimental-important.svg?style=for-the-badge) -Higher level constructs for Websocket APIs | ![Not Implemented](https://img.shields.io/badge/not--implemented-black.svg?style=for-the-badge) +Higher level constructs for Websocket APIs | ![Experimental](https://img.shields.io/badge/experimental-important.svg?style=for-the-badge) > **CFN Resources:** All classes with the `Cfn` prefix in this module ([CFN Resources]) are always > stable and safe to use. @@ -38,6 +38,7 @@ Higher level constructs for Websocket APIs | ![Not Implemented](https://img.shie - [Metrics](#metrics) - [VPC Link](#vpc-link) - [Private Integration](#private-integration) +- [WebSocket API](#websocket-api) ## Introduction @@ -230,7 +231,7 @@ API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-acces 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 +### Metrics The API Gateway v2 service sends metrics around the performance of HTTP APIs to Amazon CloudWatch. These metrics can be referred to using the metric APIs available on the `HttpApi` construct. @@ -277,3 +278,46 @@ Amazon ECS container-based applications. Using private integrations, resources clients outside of the VPC. These integrations can be found in the [APIGatewayV2-Integrations](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigatewayv2-integrations-readme.html) constructs library. + +## WebSocket API + +A WebSocket API in API Gateway is a collection of WebSocket routes that are integrated with backend HTTP endpoints, +Lambda functions, or other AWS services. You can use API Gateway features to help you with all aspects of the API +lifecycle, from creation through monitoring your production APIs. [Read more](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-overview.html) + +WebSocket APIs have two fundamental concepts - Routes and Integrations. + +WebSocket APIs direct JSON messages to backend integrations based on configured routes. (Non-JSON messages are directed +to the configured `$default` route.) + +Integrations define how the WebSocket API behaves when a client reaches a specific Route. Learn more at +[Configuring integrations](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-integration-requests.html). + +Integrations are available in the `aws-apigatewayv2-integrations` module and more information is available in that module. + +To add the default WebSocket routes supported by API Gateway (`$connect`, `$disconnect` and `$default`), configure them as part of api props: + +```ts +const webSocketApi = new WebSocketApi(stack, 'mywsapi', { + connectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: connectHandler }) }, + disconnectRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: disconnetHandler }) }, + defaultRouteOptions: { integration: new LambdaWebSocketIntegration({ handler: defaultHandler }) }, +}); + +new WebSocketStage(stack, 'mystage', { + webSocketApi, + stageName: 'dev', + autoDeploy: true, +}); +``` + +To add any other route: + +```ts +const webSocketApi = new WebSocketApi(stack, 'mywsapi'); +webSocketApi.addRoute('sendmessage', { + integration: new LambdaWebSocketIntegration({ + handler: messageHandler, + }), +}); +``` diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts index d843b51f8b315..adbe3fe3efc2c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts @@ -1,4 +1,10 @@ -import { IResource } from '@aws-cdk/core'; +import { IResource, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnApiMapping, CfnApiMappingProps } from '../apigatewayv2.generated'; +import { HttpApi } from '../http/api'; +import { IApi } from './api'; +import { IDomainName } from './domain-name'; +import { IStage } from './stage'; /** * Represents an ApiGatewayV2 ApiMapping resource @@ -11,3 +17,109 @@ export interface IApiMapping extends IResource { */ readonly apiMappingId: string; } + +/** + * Properties used to create the ApiMapping resource + */ +export interface ApiMappingProps { + /** + * Api mapping key. The path where this stage should be mapped to on the domain + * @default - undefined for the root path mapping. + */ + readonly apiMappingKey?: string; + + /** + * The Api to which this mapping is applied + */ + readonly api: IApi; + + /** + * custom domain name of the mapping target + */ + readonly domainName: IDomainName; + + /** + * stage for the ApiMapping resource + * required for WebSocket API + * defaults to default stage of an HTTP API + * + * @default - Default stage of the passed API for HTTP API, required for WebSocket API + */ + readonly stage?: IStage; +} + +/** + * The attributes used to import existing ApiMapping + */ +export interface ApiMappingAttributes { + /** + * The API mapping ID + */ + readonly apiMappingId: string; +} + +/** + * Create a new API mapping for API Gateway API endpoint. + * @resource AWS::ApiGatewayV2::ApiMapping + */ +export class ApiMapping extends Resource implements IApiMapping { + /** + * import from API ID + */ + public static fromApiMappingAttributes(scope: Construct, id: string, attrs: ApiMappingAttributes): IApiMapping { + class Import extends Resource implements IApiMapping { + public readonly apiMappingId = attrs.apiMappingId; + } + return new Import(scope, id); + } + /** + * ID of the API Mapping + */ + public readonly apiMappingId: string; + + /** + * API Mapping key + */ + public readonly mappingKey?: string; + + constructor(scope: Construct, id: string, props: ApiMappingProps) { + super(scope, id); + + let stage = props.stage; + if (!stage) { + if (props.api instanceof HttpApi) { + if (props.api.defaultStage) { + stage = props.api.defaultStage; + } else { + throw new Error('stage is required if default stage is not available'); + } + } else { + throw new Error('stage is required for WebSocket API'); + } + } + + const paramRe = '^[a-zA-Z0-9]*[-_.+!,$]?[a-zA-Z0-9]*$'; + if (props.apiMappingKey && !new RegExp(paramRe).test(props.apiMappingKey)) { + throw new Error('An ApiMapping key may contain only letters, numbers and one of $-_.+!*\'(),'); + } + + if (props.apiMappingKey === '') { + throw new Error('empty string for api mapping key not allowed'); + } + + const apiMappingProps: CfnApiMappingProps = { + apiId: props.api.apiId, + domainName: props.domainName.name, + stage: stage.stageName, + apiMappingKey: props.apiMappingKey, + }; + + const resource = new CfnApiMapping(this, 'Resource', apiMappingProps); + + // ensure the dependency on the provided stage + this.node.addDependency(stage); + + this.apiMappingId = resource.ref; + this.mappingKey = props.apiMappingKey; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts new file mode 100644 index 0000000000000..c632e6309083d --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api.ts @@ -0,0 +1,71 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import { IResource } from '@aws-cdk/core'; + +/** + * Represents a API Gateway HTTP/WebSocket API + */ +export interface IApi extends IResource { + /** + * The identifier of this API Gateway API. + * @attribute + */ + readonly apiId: string; + + /** + * The default endpoint for an API + * @attribute + */ + readonly apiEndpoint: string; + + /** + * Return the given named metric for this Api Gateway + * + * @default - average over 5 minutes + */ + metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the number of client-side errors captured in a given period. + * + * @default - sum over 5 minutes + */ + metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the number of server-side errors captured in a given period. + * + * @default - sum over 5 minutes + */ + metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the amount of data processed in bytes. + * + * @default - sum over 5 minutes + */ + metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the total number API requests in a given period. + * + * @default - SampleCount over 5 minutes + */ + metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Metric for the time between when API Gateway relays a request to the backend + * and when it receives a response from the backend. + * + * @default - no statistic + */ + metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * The time between when API Gateway receives a request from a client + * and when it returns a response to the client. + * The latency includes the integration latency and other API Gateway overhead. + * + * @default - no statistic + */ + metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/base.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base.ts new file mode 100644 index 0000000000000..542fcfb16f8f4 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/base.ts @@ -0,0 +1,111 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import { Resource } from '@aws-cdk/core'; +import { IntegrationCache } from '../private/integration-cache'; +import { IApi } from './api'; +import { ApiMapping } from './api-mapping'; +import { DomainMappingOptions, IStage } from './stage'; + +/** + * Base class representing an API + * @internal + */ +export abstract class ApiBase extends Resource implements IApi { + abstract readonly apiId: string; + abstract readonly apiEndpoint: string; + /** + * @internal + */ + protected _integrationCache: IntegrationCache = new IntegrationCache(); + + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/ApiGateway', + metricName, + dimensions: { ApiId: this.apiId }, + ...props, + }).attachTo(this); + } + + public metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('4XXError', { statistic: 'Sum', ...props }); + } + + public metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('5XXError', { statistic: 'Sum', ...props }); + } + + public metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('DataProcessed', { statistic: 'Sum', ...props }); + } + + public metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Count', { statistic: 'SampleCount', ...props }); + } + + public metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('IntegrationLatency', props); + } + + public metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Latency', props); + } +} + + +/** + * Base class representing a Stage + * @internal + */ +export abstract class StageBase extends Resource implements IStage { + public abstract readonly stageName: string; + protected abstract readonly baseApi: IApi; + + /** + * The URL to this stage. + */ + abstract get url(): string; + + /** + * @internal + */ + protected _addDomainMapping(domainMapping: DomainMappingOptions) { + new ApiMapping(this, `${domainMapping.domainName}${domainMapping.mappingKey}`, { + api: this.baseApi, + domainName: domainMapping.domainName, + stage: this, + apiMappingKey: domainMapping.mappingKey, + }); + // ensure the dependency + this.node.addDependency(domainMapping.domainName); + } + + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.baseApi.metric(metricName, props).with({ + dimensions: { ApiId: this.baseApi.apiId, Stage: this.stageName }, + }).attachTo(this); + } + + public metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('4XXError', { statistic: 'Sum', ...props }); + } + + public metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('5XXError', { statistic: 'Sum', ...props }); + } + + public metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('DataProcessed', { statistic: 'Sum', ...props }); + } + + public metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Count', { statistic: 'SampleCount', ...props }); + } + + public metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('IntegrationLatency', props); + } + + public metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('Latency', props); + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts index eeb237a4e7f84..b0a0f1c0265eb 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/index.ts @@ -1,3 +1,4 @@ +export * from './api'; export * from './integration'; export * from './route'; export * from './stage'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts index 7255607639468..83e200aadb007 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/integration.ts @@ -9,4 +9,4 @@ export interface IIntegration extends IResource { * @attribute */ readonly integrationId: string; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts index b608a7a34ad97..40b7832418633 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/stage.ts @@ -1,4 +1,6 @@ +import { Metric, MetricOptions } from '@aws-cdk/aws-cloudwatch'; import { IResource } from '@aws-cdk/core'; +import { IDomainName } from './domain-name'; /** * Represents a Stage. @@ -9,22 +11,107 @@ export interface IStage extends IResource { * @attribute */ readonly stageName: string; + + /** + * The URL to this stage. + */ + readonly url: string; + + /** + * Return the given named metric for this HTTP Api Gateway Stage + * + * @default - average over 5 minutes + */ + metric(metricName: string, props?: MetricOptions): Metric + + /** + * Metric for the number of client-side errors captured in a given period. + * + * @default - sum over 5 minutes + */ + metricClientError(props?: MetricOptions): Metric + + /** + * Metric for the number of server-side errors captured in a given period. + * + * @default - sum over 5 minutes + */ + metricServerError(props?: MetricOptions): Metric + + /** + * Metric for the amount of data processed in bytes. + * + * @default - sum over 5 minutes + */ + metricDataProcessed(props?: MetricOptions): Metric + + /** + * Metric for the total number API requests in a given period. + * + * @default - SampleCount over 5 minutes + */ + metricCount(props?: MetricOptions): Metric + + /** + * Metric for the time between when API Gateway relays a request to the backend + * and when it receives a response from the backend. + * + * @default - no statistic + */ + metricIntegrationLatency(props?: MetricOptions): Metric + + /** + * The time between when API Gateway receives a request from a client + * and when it returns a response to the client. + * The latency includes the integration latency and other API Gateway overhead. + * + * @default - no statistic + */ + metricLatency(props?: MetricOptions): Metric } /** - * Options required to create a new stage. - * Options that are common between HTTP and Websocket APIs. + * Options for DomainMapping */ -export interface CommonStageOptions { +export interface DomainMappingOptions { /** - * The name of the stage. See `StageName` class for more details. - * @default '$default' the default stage of the API. This stage will have the URL at the root of the API endpoint. + * The domain name for the mapping + * */ - readonly stageName?: string; + readonly domainName: IDomainName; + /** + * The API mapping key. Leave it undefined for the root path mapping. + * @default - empty key for the root path mapping + */ + readonly mappingKey?: string; +} + +/** + * Options required to create a new stage. + * Options that are common between HTTP and Websocket APIs. + */ +export interface StageOptions { /** * Whether updates to an API automatically trigger a new deployment. * @default false */ readonly autoDeploy?: boolean; + + /** + * The options for custom domain and api mapping + * + * @default - no custom domain and api mapping configuration + */ + readonly domainMapping?: DomainMappingOptions; +} + +/** + * The attributes used to import existing Stage + */ +export interface StageAttributes { + /** + * The name of the stage + */ + readonly stageName: string; } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts deleted file mode 100644 index ee9323240833d..0000000000000 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api-mapping.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Resource } from '@aws-cdk/core'; -import { Construct } from 'constructs'; -import { CfnApiMapping, CfnApiMappingProps } from '../apigatewayv2.generated'; -import { IApiMapping, IDomainName } from '../common'; -import { IHttpApi } from '../http/api'; -import { IHttpStage } from './stage'; - -/** - * Properties used to create the HttpApiMapping resource - */ -export interface HttpApiMappingProps { - /** - * Api mapping key. The path where this stage should be mapped to on the domain - * @default - undefined for the root path mapping. - */ - readonly apiMappingKey?: string; - - /** - * The HttpApi to which this mapping is applied - */ - readonly api: IHttpApi; - - /** - * custom domain name of the mapping target - */ - readonly domainName: IDomainName; - - /** - * stage for the HttpApiMapping resource - * - * @default - the $default stage - */ - readonly stage?: IHttpStage; -} - -/** - * The attributes used to import existing HttpApiMapping - */ -export interface HttpApiMappingAttributes { - /** - * The API mapping ID - */ - readonly apiMappingId: string; -} - -/** - * Create a new API mapping for API Gateway HTTP API endpoint. - * @resource AWS::ApiGatewayV2::ApiMapping - */ -export class HttpApiMapping extends Resource implements IApiMapping { - /** - * import from API ID - */ - public static fromHttpApiMappingAttributes(scope: Construct, id: string, attrs: HttpApiMappingAttributes): IApiMapping { - class Import extends Resource implements IApiMapping { - public readonly apiMappingId = attrs.apiMappingId; - } - return new Import(scope, id); - } - /** - * ID of the API Mapping - */ - public readonly apiMappingId: string; - - /** - * API Mapping key - */ - public readonly mappingKey?: string; - - constructor(scope: Construct, id: string, props: HttpApiMappingProps) { - super(scope, id); - - if ((!props.stage?.stageName) && !props.api.defaultStage) { - throw new Error('stage is required if default stage is not available'); - } - - const paramRe = '^[a-zA-Z0-9]*[-_.+!,$]?[a-zA-Z0-9]*$'; - if (props.apiMappingKey && !new RegExp(paramRe).test(props.apiMappingKey)) { - throw new Error('An ApiMapping key may contain only letters, numbers and one of $-_.+!*\'(),'); - } - - if (props.apiMappingKey === '') { - throw new Error('empty string for api mapping key not allowed'); - } - - const apiMappingProps: CfnApiMappingProps = { - apiId: props.api.httpApiId, - domainName: props.domainName.name, - stage: props.stage?.stageName ?? props.api.defaultStage!.stageName, - apiMappingKey: props.apiMappingKey, - }; - - const resource = new CfnApiMapping(this, 'Resource', apiMappingProps); - - // ensure the dependency on the provided stage - if (props.stage) { - this.node.addDependency(props.stage); - } - - // if stage not specified, we ensure the default stage is ready before we create the api mapping - if (!props.stage?.stageName && props.api.defaultStage) { - this.node.addDependency(props.api.defaultStage!); - } - - this.apiMappingId = resource.ref; - this.mappingKey = props.apiMappingKey; - } - -} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index 52e5f1fccbe07..b74c00e5824fb 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -1,9 +1,9 @@ -import * as crypto from 'crypto'; -import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; -import { Duration, IResource, Resource, Stack } from '@aws-cdk/core'; +import { Duration } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnApi, CfnApiProps } from '../apigatewayv2.generated'; -import { DefaultDomainMappingOptions } from '../http/stage'; +import { IApi } from '../common/api'; +import { ApiBase } from '../common/base'; +import { DomainMappingOptions, IStage } from '../common/stage'; import { IHttpRouteAuthorizer } from './authorizer'; import { IHttpRouteIntegration, HttpIntegration, HttpRouteIntegrationConfig } from './integration'; import { BatchHttpRouteOptions, HttpMethod, HttpRoute, HttpRouteKey } from './route'; @@ -13,76 +13,14 @@ import { VpcLink, VpcLinkProps } from './vpc-link'; /** * Represents an HTTP API */ -export interface IHttpApi extends IResource { +export interface IHttpApi extends IApi { /** * The identifier of this API Gateway HTTP API. * @attribute + * @deprecated - use apiId instead */ readonly httpApiId: string; - /** - * The default endpoint for an API - * @attribute - */ - readonly apiEndpoint: string; - - /** - * The default stage - */ - readonly defaultStage?: HttpStage; - - /** - * Return the given named metric for this HTTP Api Gateway - * - * @default - average over 5 minutes - */ - metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the number of client-side errors captured in a given period. - * - * @default - sum over 5 minutes - */ - metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the number of server-side errors captured in a given period. - * - * @default - sum over 5 minutes - */ - metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the amount of data processed in bytes. - * - * @default - sum over 5 minutes - */ - metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the total number API requests in a given period. - * - * @default - SampleCount over 5 minutes - */ - metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * Metric for the time between when API Gateway relays a request to the backend - * and when it receives a response from the backend. - * - * @default - no statistic - */ - metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - - /** - * The time between when API Gateway receives a request from a client - * and when it returns a response to the client. - * The latency includes the integration latency and other API Gateway overhead. - * - * @default - no statistic - */ - metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; - /** * Add a new VpcLink */ @@ -135,7 +73,7 @@ export interface HttpApiProps { * * @default - no default domain mapping configured. meaningless if `createDefaultStage` is `false`. */ - readonly defaultDomainMapping?: DefaultDomainMappingOptions; + readonly defaultDomainMapping?: DomainMappingOptions; /** * Specifies whether clients can invoke your API using the default endpoint. @@ -218,45 +156,12 @@ export interface AddRoutesOptions extends BatchHttpRouteOptions { readonly authorizationScopes?: string[]; } -abstract class HttpApiBase extends Resource implements IHttpApi { // note that this is not exported +abstract class HttpApiBase extends ApiBase implements IHttpApi { // note that this is not exported + public abstract readonly apiId: string; 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({ - namespace: 'AWS/ApiGateway', - metricName, - dimensions: { ApiId: this.httpApiId }, - ...props, - }).attachTo(this); - } - - public metricClientError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('4XXError', { statistic: 'Sum', ...props }); - } - - public metricServerError(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('5XXError', { statistic: 'Sum', ...props }); - } - - public metricDataProcessed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('DataProcessed', { statistic: 'Sum', ...props }); - } - - public metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('Count', { statistic: 'SampleCount', ...props }); - } - - public metricIntegrationLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('IntegrationLatency', props); - } - - public metricLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('Latency', props); - } public addVpcLink(options: VpcLinkProps): VpcLink { const { vpcId } = options.vpc; @@ -275,11 +180,9 @@ abstract class HttpApiBase extends Resource implements IHttpApi { // note that t * @internal */ public _addIntegration(scope: Construct, config: HttpRouteIntegrationConfig): HttpIntegration { - const stringifiedConfig = JSON.stringify(Stack.of(scope).resolve(config)); - const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); - - if (configHash in this.httpIntegrations) { - return this.httpIntegrations[configHash]; + const { configHash, integration: existingIntegration } = this._integrationCache.getIntegration(scope, config); + if (existingIntegration) { + return existingIntegration as HttpIntegration; } const integration = new HttpIntegration(scope, `HttpIntegration-${configHash}`, { @@ -291,7 +194,7 @@ abstract class HttpApiBase extends Resource implements IHttpApi { // note that t connectionType: config.connectionType, payloadFormatVersion: config.payloadFormatVersion, }); - this.httpIntegrations[configHash] = integration; + this._integrationCache.saveIntegration(scope, config, integration); return integration; } @@ -322,6 +225,7 @@ export class HttpApi extends HttpApiBase { */ public static fromHttpApiAttributes(scope: Construct, id: string, attrs: HttpApiAttributes): IHttpApi { class Import extends HttpApiBase { + public readonly apiId = attrs.httpApiId; public readonly httpApiId = attrs.httpApiId; private readonly _apiEndpoint = attrs.apiEndpoint; @@ -339,6 +243,7 @@ export class HttpApi extends HttpApiBase { * A human friendly name for this HTTP API. Note that this is different from `httpApiId`. */ public readonly httpApiName?: string; + public readonly apiId: string; public readonly httpApiId: string; /** @@ -347,9 +252,9 @@ export class HttpApi extends HttpApiBase { public readonly disableExecuteApiEndpoint?: boolean; /** - * default stage of the api resource + * The default stage of this API */ - public readonly defaultStage: HttpStage | undefined; + public readonly defaultStage: IStage | undefined; private readonly _apiEndpoint: string; @@ -392,6 +297,7 @@ export class HttpApi extends HttpApiBase { }; const resource = new CfnApi(this, 'Resource', apiProps); + this.apiId = resource.ref; this.httpApiId = resource.ref; this._apiEndpoint = resource.attrApiEndpoint; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts index aadfb630ba276..f63c2e3a96583 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts @@ -112,7 +112,7 @@ export class HttpAuthorizer extends Resource implements IHttpAuthorizer { const resource = new CfnAuthorizer(this, 'Resource', { name: props.authorizerName ?? id, - apiId: props.httpApi.httpApiId, + apiId: props.httpApi.apiId, authorizerType: props.type, identitySource: props.identitySource, jwtConfiguration: undefinedIfNoKeys({ diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts index efd60f9f24d7c..81ddfec695bc3 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts @@ -2,6 +2,5 @@ export * from './api'; export * from './route'; 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/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts index e609c9396c08f..836b831550fb7 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts @@ -134,7 +134,7 @@ export class HttpIntegration extends Resource implements IHttpIntegration { constructor(scope: Construct, id: string, props: HttpIntegrationProps) { super(scope, id); const integ = new CfnIntegration(this, 'Resource', { - apiId: props.httpApi.httpApiId, + apiId: props.httpApi.apiId, integrationType: props.integrationType, integrationUri: props.integrationUri, integrationMethod: props.method, diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts index 4510d13ed6f2b..416e9bed973a3 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts @@ -157,7 +157,7 @@ export class HttpRoute extends Resource implements IHttpRoute { } const routeProps: CfnRouteProps = { - apiId: props.httpApi.httpApiId, + apiId: props.httpApi.apiId, routeKey: props.routeKey.key, target: `integrations/${integration.integrationId}`, authorizerId: authBindResult?.authorizerId, diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts index fbe54345e25e3..d6a5f96320e3b 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/stage.ts @@ -1,11 +1,10 @@ -import { Metric, MetricOptions } from '@aws-cdk/aws-cloudwatch'; -import { Resource, Stack } from '@aws-cdk/core'; +import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnStage } from '../apigatewayv2.generated'; -import { CommonStageOptions, IDomainName, IStage } from '../common'; +import { StageOptions, IStage, StageAttributes } from '../common'; +import { IApi } from '../common/api'; +import { StageBase } from '../common/base'; import { IHttpApi } from './api'; -import { HttpApiMapping } from './api-mapping'; - const DEFAULT_STAGE_NAME = '$default'; @@ -13,18 +12,21 @@ const DEFAULT_STAGE_NAME = '$default'; * Represents the HttpStage */ export interface IHttpStage extends IStage { + /** + * The API this stage is associated to. + */ + readonly api: IHttpApi; } /** - * Options to create a new stage for an HTTP API. + * The options to create a new Stage for an HTTP API */ -export interface HttpStageOptions extends CommonStageOptions { +export interface HttpStageOptions extends StageOptions { /** - * The options for custom domain and api mapping - * - * @default - no custom domain and api mapping configuration + * The name of the stage. See `StageName` class for more details. + * @default '$default' the default stage of the API. This stage will have the URL at the root of the API endpoint. */ - readonly domainMapping?: DomainMappingOptions; + readonly stageName?: string; } /** @@ -38,51 +40,39 @@ export interface HttpStageProps extends HttpStageOptions { } /** - * Options for defaultDomainMapping + * The attributes used to import existing HttpStage */ -export interface DefaultDomainMappingOptions { - /** - * The domain name for the mapping - * - */ - readonly domainName: IDomainName; - +export interface HttpStageAttributes extends StageAttributes { /** - * The API mapping key. Leave it undefined for the root path mapping. - * @default - empty key for the root path mapping + * The API to which this stage is associated */ - readonly mappingKey?: string; -} - -/** - * Options for DomainMapping - */ -export interface DomainMappingOptions extends DefaultDomainMappingOptions { - /** - * The API Stage - * - * @default - the $default stage - */ - readonly stage?: IStage; + readonly api: IHttpApi; } /** * Represents a stage where an instance of the API is deployed. * @resource AWS::ApiGatewayV2::Stage */ -export class HttpStage extends Resource implements IStage { +export class HttpStage extends StageBase implements IHttpStage { /** * Import an existing stage into this CDK app. */ - public static fromStageName(scope: Construct, id: string, stageName: string): IStage { - class Import extends Resource implements IStage { - public readonly stageName = stageName; + public static fromHttpStageAttributes(scope: Construct, id: string, attrs: HttpStageAttributes): IHttpStage { + class Import extends StageBase implements IHttpStage { + protected readonly baseApi = attrs.api; + public readonly stageName = attrs.stageName; + public readonly api = attrs.api; + + get url(): string { + throw new Error('url is not available for imported stages.'); + } } return new Import(scope, id); } + protected readonly baseApi: IApi; public readonly stageName: string; - private httpApi: IHttpApi; + public readonly api: IHttpApi; constructor(scope: Construct, id: string, props: HttpStageProps) { super(scope, id, { @@ -90,25 +80,18 @@ export class HttpStage extends Resource implements IStage { }); new CfnStage(this, 'Resource', { - apiId: props.httpApi.httpApiId, + apiId: props.httpApi.apiId, stageName: this.physicalName, autoDeploy: props.autoDeploy, }); this.stageName = this.physicalName; - this.httpApi = props.httpApi; + this.baseApi = props.httpApi; + this.api = props.httpApi; if (props.domainMapping) { - new HttpApiMapping(this, `${props.domainMapping.domainName}${props.domainMapping.mappingKey}`, { - api: props.httpApi, - domainName: props.domainMapping.domainName, - stage: this, - apiMappingKey: props.domainMapping.mappingKey, - }); - // ensure the dependency - this.node.addDependency(props.domainMapping.domainName); + this._addDomainMapping(props.domainMapping); } - } /** @@ -117,75 +100,6 @@ export class HttpStage extends Resource implements IStage { public get url(): string { const s = Stack.of(this); const urlPath = this.stageName === DEFAULT_STAGE_NAME ? '' : this.stageName; - return `https://${this.httpApi.httpApiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; - } - - /** - * Return the given named metric for this HTTP Api Gateway Stage - * - * @default - average over 5 minutes - */ - public metric(metricName: string, props?: MetricOptions): Metric { - var api = this.httpApi; - return api.metric(metricName, props).with({ - dimensions: { ApiId: this.httpApi.httpApiId, Stage: this.stageName }, - }).attachTo(this); - } - - /** - * Metric for the number of client-side errors captured in a given period. - * - * @default - sum over 5 minutes - */ - public metricClientError(props?: MetricOptions): Metric { - return this.metric('4XXError', { statistic: 'Sum', ...props }); - } - - /** - * Metric for the number of server-side errors captured in a given period. - * - * @default - sum over 5 minutes - */ - public metricServerError(props?: MetricOptions): Metric { - return this.metric('5XXError', { statistic: 'Sum', ...props }); - } - - /** - * Metric for the amount of data processed in bytes. - * - * @default - sum over 5 minutes - */ - public metricDataProcessed(props?: MetricOptions): Metric { - return this.metric('DataProcessed', { statistic: 'Sum', ...props }); - } - - /** - * Metric for the total number API requests in a given period. - * - * @default - SampleCount over 5 minutes - */ - public metricCount(props?: MetricOptions): Metric { - return this.metric('Count', { statistic: 'SampleCount', ...props }); - } - - /** - * Metric for the time between when API Gateway relays a request to the backend - * and when it receives a response from the backend. - * - * @default - no statistic - */ - public metricIntegrationLatency(props?: MetricOptions): Metric { - return this.metric('IntegrationLatency', props); - } - - /** - * The time between when API Gateway receives a request from a client - * and when it returns a response to the client. - * The latency includes the integration latency and other API Gateway overhead. - * - * @default - no statistic - */ - public metricLatency(props?: MetricOptions): Metric { - return this.metric('Latency', props); + return `https://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts index 31ea86b4a91c2..12dd8113f8b4c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/index.ts @@ -1,3 +1,4 @@ export * from './apigatewayv2.generated'; export * from './common'; -export * from './http'; \ No newline at end of file +export * from './http'; +export * from './websocket'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/private/integration-cache.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/private/integration-cache.ts new file mode 100644 index 0000000000000..2401d28e20d2d --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/private/integration-cache.ts @@ -0,0 +1,29 @@ +import * as crypto from 'crypto'; +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IIntegration } from '../common/integration'; +import { HttpRouteIntegrationConfig } from '../http'; +import { WebSocketRouteIntegrationConfig } from '../websocket'; + +type IntegrationConfig = HttpRouteIntegrationConfig | WebSocketRouteIntegrationConfig; + +export class IntegrationCache { + private integrations: Record = {}; + + getIntegration(scope: Construct, config: IntegrationConfig) { + const configHash = this.integrationConfigHash(scope, config); + const integration = this.integrations[configHash]; + return { configHash, integration }; + } + + saveIntegration(scope: Construct, config: IntegrationConfig, integration: IIntegration) { + const configHash = this.integrationConfigHash(scope, config); + this.integrations[configHash] = integration; + } + + private integrationConfigHash(scope: Construct, config: IntegrationConfig): string { + const stringifiedConfig = JSON.stringify(Stack.of(scope).resolve(config)); + const configHash = crypto.createHash('md5').update(stringifiedConfig).digest('hex'); + return configHash; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts new file mode 100644 index 0000000000000..f2f2653c94ee6 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/api.ts @@ -0,0 +1,130 @@ +import { Construct } from 'constructs'; +import { CfnApi } from '../apigatewayv2.generated'; +import { IApi } from '../common/api'; +import { ApiBase } from '../common/base'; +import { WebSocketRouteIntegrationConfig, WebSocketIntegration } from './integration'; +import { WebSocketRoute, WebSocketRouteOptions } from './route'; + +/** + * Represents a WebSocket API + */ +export interface IWebSocketApi extends IApi { + /** + * Add a websocket integration + * @internal + */ + _addIntegration(scope: Construct, config: WebSocketRouteIntegrationConfig): WebSocketIntegration +} + +/** + * Props for WebSocket API + */ +export interface WebSocketApiProps { + /** + * Name for the WebSocket API resoruce + * @default - id of the WebSocketApi construct. + */ + readonly apiName?: string; + + /** + * The description of the API. + * @default - none + */ + readonly description?: string; + + /** + * The route selection expression for the API + * @default '$request.body.action' + */ + readonly routeSelectionExpression?: string; + + /** + * Options to configure a '$connect' route + * + * @default - no '$connect' route configured + */ + readonly connectRouteOptions?: WebSocketRouteOptions; + + /** + * Options to configure a '$disconnect' route + * + * @default - no '$disconnect' route configured + */ + readonly disconnectRouteOptions?: WebSocketRouteOptions; + + /** + * Options to configure a '$default' route + * + * @default - no '$default' route configured + */ + readonly defaultRouteOptions?: WebSocketRouteOptions; +} + +/** + * Create a new API Gateway WebSocket API endpoint. + * @resource AWS::ApiGatewayV2::Api + */ +export class WebSocketApi extends ApiBase implements IWebSocketApi { + public readonly apiId: string; + public readonly apiEndpoint: string; + + /** + * A human friendly name for this WebSocket API. Note that this is different from `webSocketApiId`. + */ + public readonly webSocketApiName?: string; + + constructor(scope: Construct, id: string, props?: WebSocketApiProps) { + super(scope, id); + + this.webSocketApiName = props?.apiName ?? id; + + const resource = new CfnApi(this, 'Resource', { + name: this.webSocketApiName, + protocolType: 'WEBSOCKET', + description: props?.description, + routeSelectionExpression: props?.routeSelectionExpression ?? '$request.body.action', + }); + this.apiId = resource.ref; + this.apiEndpoint = resource.attrApiEndpoint; + + if (props?.connectRouteOptions) { + this.addRoute('$connect', props.connectRouteOptions); + } + if (props?.disconnectRouteOptions) { + this.addRoute('$disconnect', props.disconnectRouteOptions); + } + if (props?.defaultRouteOptions) { + this.addRoute('$default', props.defaultRouteOptions); + } + } + + /** + * @internal + */ + public _addIntegration(scope: Construct, config: WebSocketRouteIntegrationConfig): WebSocketIntegration { + const { configHash, integration: existingIntegration } = this._integrationCache.getIntegration(scope, config); + if (existingIntegration) { + return existingIntegration as WebSocketIntegration; + } + + const integration = new WebSocketIntegration(scope, `WebSocketIntegration-${configHash}`, { + webSocketApi: this, + integrationType: config.type, + integrationUri: config.uri, + }); + this._integrationCache.saveIntegration(scope, config, integration); + + return integration; + } + + /** + * Add a new route + */ + public addRoute(routeKey: string, options: WebSocketRouteOptions) { + return new WebSocketRoute(this, `${routeKey}-Route`, { + webSocketApi: this, + routeKey, + ...options, + }); + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/index.ts new file mode 100644 index 0000000000000..b0ce6a8a91419 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/index.ts @@ -0,0 +1,4 @@ +export * from './api'; +export * from './route'; +export * from './stage'; +export * from './integration'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts new file mode 100644 index 0000000000000..e75bd00b63d95 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/integration.ts @@ -0,0 +1,110 @@ +import { Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnIntegration } from '../apigatewayv2.generated'; +import { IIntegration } from '../common'; +import { IWebSocketApi } from './api'; +import { IWebSocketRoute } from './route'; + +// 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'; + +/** + * Represents an Integration for an WebSocket API. + */ +export interface IWebSocketIntegration extends IIntegration { + /** The WebSocket API associated with this integration */ + readonly webSocketApi: IWebSocketApi; +} + +/** + * WebSocket Integration Types + */ +export enum WebSocketIntegrationType { + /** + * AWS Proxy Integration Type + */ + AWS_PROXY = 'AWS_PROXY' +} + +/** + * The integration properties + */ +export interface WebSocketIntegrationProps { + /** + * The WebSocket API to which this integration should be bound. + */ + readonly webSocketApi: IWebSocketApi; + + /** + * Integration type + */ + readonly integrationType: WebSocketIntegrationType; + + /** + * Integration URI. + */ + readonly integrationUri: string; +} + +/** + * The integration for an API route. + * @resource AWS::ApiGatewayV2::Integration + */ +export class WebSocketIntegration extends Resource implements IWebSocketIntegration { + public readonly integrationId: string; + public readonly webSocketApi: IWebSocketApi; + + constructor(scope: Construct, id: string, props: WebSocketIntegrationProps) { + super(scope, id); + const integ = new CfnIntegration(this, 'Resource', { + apiId: props.webSocketApi.apiId, + integrationType: props.integrationType, + integrationUri: props.integrationUri, + }); + this.integrationId = integ.ref; + this.webSocketApi = props.webSocketApi; + } +} + +/** + * Options to the WebSocketRouteIntegration during its bind operation. + */ +export interface WebSocketRouteIntegrationBindOptions { + /** + * The route to which this is being bound. + */ + readonly route: IWebSocketRoute; + + /** + * The current scope in which the bind is occurring. + * If the `WebSocketRouteIntegration` being bound creates additional constructs, + * this will be used as their parent scope. + */ + readonly scope: CoreConstruct; +} + +/** + * The interface that various route integration classes will inherit. + */ +export interface IWebSocketRouteIntegration { + /** + * Bind this integration to the route. + */ + bind(options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig; +} + +/** + * Config returned back as a result of the bind. + */ +export interface WebSocketRouteIntegrationConfig { + /** + * Integration type. + */ + readonly type: WebSocketIntegrationType; + + /** + * Integration URI + */ + readonly uri: string; +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts new file mode 100644 index 0000000000000..0588889a603bc --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/route.ts @@ -0,0 +1,84 @@ +import { Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnRoute } from '../apigatewayv2.generated'; +import { IRoute } from '../common'; +import { IWebSocketApi } from './api'; +import { IWebSocketRouteIntegration } from './integration'; + +/** + * Represents a Route for an WebSocket API. + */ +export interface IWebSocketRoute extends IRoute { + /** + * The WebSocket API associated with this route. + */ + readonly webSocketApi: IWebSocketApi; + + /** + * The key to this route. + * @attribute + */ + readonly routeKey: string; +} + +/** + * Options used to add route to the API + */ +export interface WebSocketRouteOptions { + /** + * The integration to be configured on this route. + */ + readonly integration: IWebSocketRouteIntegration; +} + + +/** + * Properties to initialize a new Route + */ +export interface WebSocketRouteProps extends WebSocketRouteOptions { + /** + * the API the route is associated with + */ + readonly webSocketApi: IWebSocketApi; + + /** + * The key to this route. + */ + readonly routeKey: string; +} + +/** + * Route class that creates the Route for API Gateway WebSocket API + * @resource AWS::ApiGatewayV2::Route + */ +export class WebSocketRoute extends Resource implements IWebSocketRoute { + public readonly routeId: string; + public readonly webSocketApi: IWebSocketApi; + public readonly routeKey: string; + + /** + * Integration response ID + */ + public readonly integrationResponseId?: string; + + constructor(scope: Construct, id: string, props: WebSocketRouteProps) { + super(scope, id); + + this.webSocketApi = props.webSocketApi; + this.routeKey = props.routeKey; + + const config = props.integration.bind({ + route: this, + scope: this, + }); + + const integration = props.webSocketApi._addIntegration(this, config); + + const route = new CfnRoute(this, 'Resource', { + apiId: props.webSocketApi.apiId, + routeKey: props.routeKey, + target: `integrations/${integration.integrationId}`, + }); + this.routeId = route.ref; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts new file mode 100644 index 0000000000000..a50353a79ca2d --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts @@ -0,0 +1,96 @@ +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnStage } from '../apigatewayv2.generated'; +import { StageOptions, IApi, IStage, StageAttributes } from '../common'; +import { StageBase } from '../common/base'; +import { IWebSocketApi } from './api'; + +/** + * Represents the WebSocketStage + */ +export interface IWebSocketStage extends IStage { + /** + * The API this stage is associated to. + */ + readonly api: IWebSocketApi; +} + +/** + * Properties to initialize an instance of `WebSocketStage`. + */ +export interface WebSocketStageProps extends StageOptions { + /** + * The WebSocket API to which this stage is associated. + */ + readonly webSocketApi: IWebSocketApi; + + /** + * The name of the stage. + */ + readonly stageName: string; +} + +/** + * The attributes used to import existing WebSocketStage + */ +export interface WebSocketStageAttributes extends StageAttributes { + /** + * The API to which this stage is associated + */ + readonly api: IWebSocketApi; +} + +/** + * Represents a stage where an instance of the API is deployed. + * @resource AWS::ApiGatewayV2::Stage + */ +export class WebSocketStage extends StageBase implements IWebSocketStage { + /** + * Import an existing stage into this CDK app. + */ + public static fromWebSocketStageAttributes(scope: Construct, id: string, attrs: WebSocketStageAttributes): IWebSocketStage { + class Import extends StageBase implements IWebSocketStage { + public readonly baseApi = attrs.api; + public readonly stageName = attrs.stageName; + public readonly api = attrs.api; + + get url(): string { + throw new Error('url is not available for imported stages.'); + } + } + return new Import(scope, id); + } + + protected readonly baseApi: IApi; + public readonly stageName: string; + public readonly api: IWebSocketApi; + + constructor(scope: Construct, id: string, props: WebSocketStageProps) { + super(scope, id, { + physicalName: props.stageName, + }); + + this.baseApi = props.webSocketApi; + this.api = props.webSocketApi; + this.stageName = this.physicalName; + + new CfnStage(this, 'Resource', { + apiId: props.webSocketApi.apiId, + stageName: this.physicalName, + autoDeploy: props.autoDeploy, + }); + + if (props.domainMapping) { + this._addDomainMapping(props.domainMapping); + } + } + + /** + * The URL to this stage. + */ + public get url(): string { + const s = Stack.of(this); + const urlPath = this.stageName; + return `wss://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 19abb0ca10b3f..b1dd874b85f9e 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -103,13 +103,16 @@ }, "awslint": { "exclude": [ + "props-physical-name:@aws-cdk/aws-apigatewayv2.ApiMappingProps", "from-method:@aws-cdk/aws-apigatewayv2.HttpIntegration", "from-method:@aws-cdk/aws-apigatewayv2.HttpRoute", - "from-method:@aws-cdk/aws-apigatewayv2.HttpStage", - "props-physical-name-type:@aws-cdk/aws-apigatewayv2.HttpStageProps.stageName", - "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpApiMappingProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpIntegrationProps", - "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpRouteProps" + "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpRouteProps", + "from-method:@aws-cdk/aws-apigatewayv2.WebSocketApi", + "from-method:@aws-cdk/aws-apigatewayv2.WebSocketIntegration", + "from-method:@aws-cdk/aws-apigatewayv2.WebSocketRoute", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketIntegrationProps", + "props-physical-name:@aws-cdk/aws-apigatewayv2.WebSocketRouteProps" ] }, "stability": "experimental", @@ -121,7 +124,7 @@ }, { "name": "Higher level constructs for Websocket APIs", - "stability": "Not Implemented" + "stability": "Experimental" } ], "awscdkio": { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api-mapping.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts similarity index 76% rename from packages/@aws-cdk/aws-apigatewayv2/test/http/api-mapping.test.ts rename to packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts index fe113727c3f50..b917f19513a57 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api-mapping.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts @@ -1,7 +1,7 @@ import '@aws-cdk/assert/jest'; import { Certificate } from '@aws-cdk/aws-certificatemanager'; import { Stack } from '@aws-cdk/core'; -import { DomainName, HttpApi, HttpApiMapping } from '../../lib'; +import { DomainName, HttpApi, ApiMapping, WebSocketApi } from '../../lib'; const domainName = 'example.com'; const certArn = 'arn:aws:acm:us-east-1:111111111111:certificate'; @@ -17,7 +17,7 @@ describe('ApiMapping', () => { certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), }); - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, }); @@ -47,7 +47,7 @@ describe('ApiMapping', () => { certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), }); - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, stage: beta, @@ -75,7 +75,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: '', @@ -94,7 +94,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: '/', @@ -113,7 +113,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: '/foo', @@ -132,7 +132,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: 'foo/bar', @@ -151,7 +151,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: 'foo/', @@ -170,7 +170,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: '^foo', @@ -189,7 +189,7 @@ describe('ApiMapping', () => { }); expect(() => { - new HttpApiMapping(stack, 'Mapping', { + new ApiMapping(stack, 'Mapping', { api, domainName: dn, apiMappingKey: 'foo.*$', @@ -207,15 +207,53 @@ describe('ApiMapping', () => { certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), }); - const mapping = new HttpApiMapping(stack, 'Mapping', { + const mapping = new ApiMapping(stack, 'Mapping', { api, domainName: dn, }); - const imported = HttpApiMapping.fromHttpApiMappingAttributes(stack, 'ImportedMapping', { + const imported = ApiMapping.fromApiMappingAttributes(stack, 'ImportedMapping', { apiMappingId: mapping.apiMappingId, } ); expect(imported.apiMappingId).toEqual(mapping.apiMappingId); }); + + test('stage validation - throws if defaultStage not available for HttpApi', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'Api', { + createDefaultStage: false, + }); + const dn = new DomainName(stack, 'DomainName', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + + // WHEN + expect(() => { + new ApiMapping(stack, 'Mapping', { + api, + domainName: dn, + }); + }).toThrow(/stage is required if default stage is not available/); + }); + + test('stage validation - throws if stage not provided for WebSocketApi', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api'); + const dn = new DomainName(stack, 'DomainName', { + domainName, + certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), + }); + + // WHEN + expect(() => { + new ApiMapping(stack, 'Mapping', { + api, + domainName: dn, + }); + }).toThrow(/stage is required for WebSocket API/); + }); }); 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 01252be7d84f1..a8c5f418f7782 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -19,7 +19,7 @@ describe('HttpApi', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Stage', { - ApiId: stack.resolve(api.httpApiId), + ApiId: stack.resolve(api.apiId), StageName: '$default', AutoDeploy: true, }); @@ -34,7 +34,7 @@ describe('HttpApi', () => { const stack = new Stack(); const imported = HttpApi.fromHttpApiAttributes(stack, 'imported', { httpApiId: 'http-1234', apiEndpoint: 'api-endpoint' }); - expect(imported.httpApiId).toEqual('http-1234'); + expect(imported.apiId).toEqual('http-1234'); expect(imported.apiEndpoint).toEqual('api-endpoint'); }); @@ -55,12 +55,12 @@ describe('HttpApi', () => { }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), RouteKey: '$default', }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Integration', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), }); }); @@ -75,12 +75,12 @@ describe('HttpApi', () => { }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), RouteKey: 'GET /pets', }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), RouteKey: 'PATCH /pets', }); }); @@ -95,7 +95,7 @@ describe('HttpApi', () => { }); expect(stack).toHaveResourceLike('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), RouteKey: 'ANY /pets', }); }); @@ -149,7 +149,7 @@ describe('HttpApi', () => { }); const metricName = '4xxError'; const statistic = 'Sum'; - const apiId = api.httpApiId; + const apiId = api.apiId; // WHEN const countMetric = api.metric(metricName, { statistic }); @@ -168,7 +168,7 @@ describe('HttpApi', () => { createDefaultStage: false, }); const color = '#00ff00'; - const apiId = api.httpApiId; + const apiId = api.apiId; // WHEN const metrics = new Array(); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/authorizer.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/authorizer.test.ts index bee2f7d05be1b..afb98fe733f19 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/authorizer.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/authorizer.test.ts @@ -18,7 +18,7 @@ describe('HttpAuthorizer', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Authorizer', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), Name: 'HttpAuthorizer', AuthorizerType: 'JWT', IdentitySource: ['identitysource.1', 'identitysource.2'], 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 a5ce1b7b64b64..41af3121dc893 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts @@ -17,7 +17,7 @@ describe('HttpRoute', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), RouteKey: 'GET /books', Target: { 'Fn::Join': [ @@ -33,7 +33,7 @@ describe('HttpRoute', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), }); }); @@ -48,7 +48,7 @@ describe('HttpRoute', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), IntegrationType: 'HTTP_PROXY', PayloadFormatVersion: '2.0', IntegrationUri: 'some-uri', @@ -209,7 +209,7 @@ describe('HttpRoute', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { - ApiId: stack.resolve(httpApi.httpApiId), + ApiId: stack.resolve(httpApi.apiId), IntegrationType: 'HTTP_PROXY', PayloadFormatVersion: '2.0', IntegrationUri: 'some-uri', diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts index 6c4359b5439c9..ff70ea026acb6 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/stage.test.ts @@ -16,7 +16,7 @@ describe('HttpStage', () => { }); expect(stack).toHaveResource('AWS::ApiGatewayV2::Stage', { - ApiId: stack.resolve(api.httpApiId), + ApiId: stack.resolve(api.apiId), StageName: '$default', }); }); @@ -31,7 +31,10 @@ describe('HttpStage', () => { httpApi: api, }); - const imported = HttpStage.fromStageName(stack, 'Import', stage.stageName ); + const imported = HttpStage.fromHttpStageAttributes(stack, 'Import', { + stageName: stage.stageName, + api, + }); expect(imported.stageName).toEqual(stage.stageName); }); @@ -66,7 +69,7 @@ describe('HttpStage', () => { }); const metricName = '4xxError'; const statistic = 'Sum'; - const apiId = api.httpApiId; + const apiId = api.apiId; // WHEN const countMetric = stage.metric(metricName, { statistic }); @@ -91,7 +94,7 @@ describe('HttpStage', () => { httpApi: api, }); const color = '#00ff00'; - const apiId = api.httpApiId; + const apiId = api.apiId; // WHEN const metrics = new Array(); diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts new file mode 100644 index 0000000000000..fcc65d4e18207 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/api.test.ts @@ -0,0 +1,92 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { + IWebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType, + WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, +} from '../../lib'; + +describe('WebSocketApi', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new WebSocketApi(stack, 'api'); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Api', { + Name: 'api', + ProtocolType: 'WEBSOCKET', + }); + + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::Stage'); + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::Route'); + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::Integration'); + }); + + test('addRoute: adds a route with passed key', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api'); + + // WHEN + api.addRoute('myroute', { integration: new DummyIntegration() }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(api.apiId), + RouteKey: 'myroute', + }); + }); + + test('connectRouteOptions: adds a $connect route', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api', { + connectRouteOptions: { integration: new DummyIntegration() }, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(api.apiId), + RouteKey: '$connect', + }); + }); + + test('disconnectRouteOptions: adds a $disconnect route', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api', { + disconnectRouteOptions: { integration: new DummyIntegration() }, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(api.apiId), + RouteKey: '$disconnect', + }); + }); + + test('defaultRouteOptions: adds a $default route', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'api', { + defaultRouteOptions: { integration: new DummyIntegration() }, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(api.apiId), + RouteKey: '$default', + }); + }); +}); + +class DummyIntegration implements IWebSocketRouteIntegration { + bind(_options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + return { + type: WebSocketIntegrationType.AWS_PROXY, + uri: 'some-uri', + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts new file mode 100644 index 0000000000000..04e8e5fc7efac --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/route.test.ts @@ -0,0 +1,54 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { + IWebSocketRouteIntegration, WebSocketApi, WebSocketIntegrationType, + WebSocketRoute, WebSocketRouteIntegrationBindOptions, WebSocketRouteIntegrationConfig, +} from '../../lib'; + +describe('WebSocketRoute', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const webSocketApi = new WebSocketApi(stack, 'Api'); + + // WHEN + new WebSocketRoute(stack, 'Route', { + webSocketApi, + integration: new DummyIntegration(), + routeKey: 'message', + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + ApiId: stack.resolve(webSocketApi.apiId), + RouteKey: 'message', + Target: { + 'Fn::Join': [ + '', + [ + 'integrations/', + { + Ref: 'RouteWebSocketIntegrationb7742333c7ab20d7b2b178df59bb17f20338431E', + }, + ], + ], + }, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + ApiId: stack.resolve(webSocketApi.apiId), + IntegrationType: 'AWS_PROXY', + IntegrationUri: 'some-uri', + }); + }); +}); + + +class DummyIntegration implements IWebSocketRouteIntegration { + bind(_options: WebSocketRouteIntegrationBindOptions): WebSocketRouteIntegrationConfig { + return { + type: WebSocketIntegrationType.AWS_PROXY, + uri: 'some-uri', + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts new file mode 100644 index 0000000000000..5ebdf0c61a980 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts @@ -0,0 +1,44 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { WebSocketApi, WebSocketStage } from '../../lib'; + +describe('WebSocketStage', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + const defaultStage = new WebSocketStage(stack, 'Stage', { + webSocketApi: api, + stageName: 'dev', + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Stage', { + ApiId: stack.resolve(api.apiId), + StageName: 'dev', + }); + expect(defaultStage.url.endsWith('/dev')).toBe(true); + }); + + test('import', () => { + // GIVEN + const stack = new Stack(); + const api = new WebSocketApi(stack, 'Api'); + + // WHEN + const stage = new WebSocketStage(stack, 'Stage', { + webSocketApi: api, + stageName: 'dev', + }); + + const imported = WebSocketStage.fromWebSocketStageAttributes(stack, 'Import', { + stageName: stage.stageName, + api, + }); + + // THEN + expect(imported.stageName).toEqual(stage.stageName); + }); +}); diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts index c576e17453557..cdeb00be9a426 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts @@ -17,6 +17,13 @@ export abstract class Schedule { * Construct a schedule from an interval and a time unit */ public static rate(duration: Duration): Schedule { + const validDurationUnit = ['minute', 'minutes', 'hour', 'hours', 'day', 'days']; + if (!validDurationUnit.includes(duration.unitLabel())) { + throw new Error("Allowed unit for scheduling is: 'minute', 'minutes', 'hour', 'hours', 'day' or 'days'"); + } + if (duration.isUnresolved()) { + return new LiteralSchedule(`rate(${duration.formatTokenToNumber()})`); + } if (duration.toSeconds() === 0) { throw new Error('Duration cannot be 0'); } diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts index 6af9c525c9bf9..ca0fb69812b10 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts @@ -1,4 +1,4 @@ -import { Duration } from '@aws-cdk/core'; +import { Duration, Stack, Lazy } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as appscaling from '../lib'; @@ -15,8 +15,15 @@ export = { 'rate must be whole number of minutes'(test: Test) { test.throws(() => { - appscaling.Schedule.rate(Duration.seconds(12345)); - }, /'12345 seconds' cannot be converted into a whole number of minutes/); + appscaling.Schedule.rate(Duration.minutes(0.13456)); + }, /'0.13456 minutes' cannot be converted into a whole number of seconds/); + test.done(); + }, + + 'rate must not be in seconds'(test: Test) { + test.throws(() => { + appscaling.Schedule.rate(Duration.seconds(1)); + }, /Allowed unit for scheduling is: 'minute', 'minutes', 'hour', 'hours', 'day' or 'days'/); test.done(); }, @@ -26,4 +33,18 @@ export = { }, /Duration cannot be 0/); test.done(); }, + + 'rate can be token'(test: Test) { + const stack = new Stack(); + const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 5 })); + const rate = appscaling.Schedule.rate(lazyDuration); + test.equal('rate(5 minutes)', stack.resolve(rate).expressionString); + test.done(); + }, + + 'rate can be in allowed type hours'(test: Test) { + test.equal('rate(1 hour)', appscaling.Schedule.rate(Duration.hours(1)) + .expressionString); + test.done(); + }, }; 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 d0f82b65a940e..0190f7771c88e 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 @@ -37,7 +37,7 @@ "eslint-plugin-standard": "^4.1.0", "jest": "^26.6.3", "lambda-tester": "^3.6.0", - "nock": "^13.0.9", - "ts-jest": "^26.5.2" + "nock": "^13.0.10", + "ts-jest": "^26.5.3" } } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts b/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts index 18924ade79748..e9dbf46901eda 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts @@ -231,6 +231,9 @@ export class CacheHeaderBehavior { if (headers.length === 0) { throw new Error('At least one header to allow must be provided'); } + if (headers.length > 10) { + throw new Error(`Maximum allowed headers in Cache Policy is 10; got ${headers.length}.`); + } return new CacheHeaderBehavior('whitelist', headers); } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin-request-policy.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin-request-policy.ts index 3d45cf2bc56fb..17e7894e6e84e 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin-request-policy.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin-request-policy.ts @@ -121,7 +121,7 @@ export class OriginRequestPolicy extends Resource implements IOriginRequestPolic } /** - * Ddetermines whether any cookies in viewer requests (and if so, which cookies) + * Determines whether any cookies in viewer requests (and if so, which cookies) * are included in requests that CloudFront sends to the origin. */ export class OriginRequestCookieBehavior { @@ -184,6 +184,12 @@ export class OriginRequestHeaderBehavior { if (headers.length === 0) { throw new Error('At least one header to allow must be provided'); } + if (headers.length > 10) { + throw new Error(`Maximum allowed headers in Origin Request Policy is 10; got ${headers.length}.`); + } + if (/Authorization/i.test(headers.join('|')) || /Accept-Encoding/i.test(headers.join('|'))) { + throw new Error('you cannot pass `Authorization` or `Accept-Encoding` as header values; use a CachePolicy to forward these headers instead'); + } return new OriginRequestHeaderBehavior('whitelist', headers); } diff --git a/packages/@aws-cdk/aws-cloudfront/test/cache-policy.test.ts b/packages/@aws-cdk/aws-cloudfront/test/cache-policy.test.ts index 66b5fa80d5749..4f6d618c51621 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/cache-policy.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/cache-policy.test.ts @@ -96,6 +96,17 @@ describe('CachePolicy', () => { expect(() => new CachePolicy(stack, 'CachePolicy6', { cachePolicyName: 'My_Policy' })).not.toThrow(); }); + test('throws if more than 10 CacheHeaderBehavior headers are being passed', () => { + const errorMessage = /Maximum allowed headers in Cache Policy is 10; got (.*?)/; + expect(() => new CachePolicy(stack, 'CachePolicy1', { + headerBehavior: CacheHeaderBehavior.allowList('Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do', 'eiusmod'), + })).toThrow(errorMessage); + + expect(() => new CachePolicy(stack, 'CachePolicy2', { + headerBehavior: CacheHeaderBehavior.allowList('Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do'), + })).not.toThrow(); + }); + test('does not throw if cachePolicyName is a token', () => { expect(() => new CachePolicy(stack, 'CachePolicy', { cachePolicyName: Aws.STACK_NAME, diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin-request-policy.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin-request-policy.test.ts index e719225c4845c..b342ac434e48e 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/origin-request-policy.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/origin-request-policy.test.ts @@ -77,6 +77,29 @@ describe('OriginRequestPolicy', () => { expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy6', { originRequestPolicyName: 'My_Policy' })).not.toThrow(); }); + test('throws if prohibited headers are being passed', () => { + const errorMessage = /you cannot pass `Authorization` or `Accept-Encoding` as header values/; + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy1', { headerBehavior: OriginRequestHeaderBehavior.allowList('Authorization') })).toThrow(errorMessage); + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy2', { headerBehavior: OriginRequestHeaderBehavior.allowList('Accept-Encoding') })).toThrow(errorMessage); + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy3', { headerBehavior: OriginRequestHeaderBehavior.allowList('authorization') })).toThrow(errorMessage); + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy4', { headerBehavior: OriginRequestHeaderBehavior.allowList('accept-encoding') })).toThrow(errorMessage); + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy5', { headerBehavior: OriginRequestHeaderBehavior.allowList('Foo', 'Authorization', 'Bar') })).toThrow(errorMessage); + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy6', { headerBehavior: OriginRequestHeaderBehavior.allowList('Foo', 'Accept-Encoding', 'Bar') })).toThrow(errorMessage); + + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy7', { headerBehavior: OriginRequestHeaderBehavior.allowList('Foo', 'Bar') })).not.toThrow(); + }); + + test('throws if more than 10 OriginRequestHeaderBehavior headers are being passed', () => { + const errorMessage = /Maximum allowed headers in Origin Request Policy is 10; got (.*?)/; + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy1', { + headerBehavior: OriginRequestHeaderBehavior.allowList('Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do', 'eiusmod'), + })).toThrow(errorMessage); + + expect(() => new OriginRequestPolicy(stack, 'OriginRequestPolicy2', { + headerBehavior: OriginRequestHeaderBehavior.allowList('Lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit', 'sed', 'do'), + })).not.toThrow(); + }); + test('does not throw if originRequestPolicyName is a token', () => { expect(() => new OriginRequestPolicy(stack, 'CachePolicy', { originRequestPolicyName: Aws.STACK_NAME, diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/README.md b/packages/@aws-cdk/aws-cloudwatch-actions/README.md index 03d84220c5c08..f13861a8c1555 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/README.md +++ b/packages/@aws-cdk/aws-cloudwatch-actions/README.md @@ -11,4 +11,19 @@ This library contains a set of classes which can be used as CloudWatch Alarm actions. +The currently implemented actions are: EC2 Actions, SNS Actions, Autoscaling Actions and Aplication Autoscaling Actions + + +## EC2 Action Example + +```ts +import * as cw from "@aws-cdk/aws-cloudwatch"; +// Alarm must be configured with an EC2 per-instance metric +let alarm: cw.Alarm; +// Attach a reboot when alarm triggers +alarm.addAlarmAction( + new Ec2Action(Ec2InstanceActions.REBOOT) +); +``` + See `@aws-cdk/aws-cloudwatch` for more information. diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/lib/ec2.ts b/packages/@aws-cdk/aws-cloudwatch-actions/lib/ec2.ts new file mode 100644 index 0000000000000..57d5a4fb67501 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch-actions/lib/ec2.ts @@ -0,0 +1,47 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +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'; + +/** + * Types of EC2 actions available + */ +export enum Ec2InstanceAction { + /** + * Stop the instance + */ + STOP = 'stop', + /** + * Terminatethe instance + */ + TERMINATE = 'terminate', + /** + * Recover the instance + */ + RECOVER = 'recover', + /** + * Reboot the instance + */ + REBOOT = 'reboot' +} + +/** + * Use an EC2 action as an Alarm action + */ +export class Ec2Action implements cloudwatch.IAlarmAction { + private ec2Action: Ec2InstanceAction; + + constructor(instanceAction: Ec2InstanceAction) { + this.ec2Action = instanceAction; + } + + /** + * Returns an alarm action configuration to use an EC2 action as an alarm action + */ + bind(_scope: Construct, _alarm: cloudwatch.IAlarm): cloudwatch.AlarmActionConfig { + return { alarmActionArn: `arn:aws:automate:${Stack.of(_scope).region}:ec2:${this.ec2Action}` }; + } +} + diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/lib/index.ts b/packages/@aws-cdk/aws-cloudwatch-actions/lib/index.ts index 3c446d47491aa..5a384eba01247 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudwatch-actions/lib/index.ts @@ -1,3 +1,4 @@ export * from './appscaling'; export * from './autoscaling'; export * from './sns'; +export * from './ec2'; diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/test/ec2.test.ts b/packages/@aws-cdk/aws-cloudwatch-actions/test/ec2.test.ts new file mode 100644 index 0000000000000..8845588d533b4 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch-actions/test/ec2.test.ts @@ -0,0 +1,41 @@ +import '@aws-cdk/assert/jest'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import { Stack } from '@aws-cdk/core'; +import * as actions from '../lib'; + +test('can use instance reboot as alarm action', () => { + // GIVEN + const stack = new Stack(); + const alarm = new cloudwatch.Alarm(stack, 'Alarm', { + metric: new cloudwatch.Metric({ + namespace: 'AWS/EC2', + metricName: 'StatusCheckFailed', + dimensions: { + InstanceId: 'i-03cb889aaaafffeee', + }, + }), + evaluationPeriods: 3, + threshold: 100, + }); + + // WHEN + alarm.addAlarmAction(new actions.Ec2Action(actions.Ec2InstanceAction.REBOOT)); + + // THEN + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { + AlarmActions: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:automate:', + { + Ref: 'AWS::Region', + }, + ':ec2:reboot', + ], + ], + }, + ], + }); +}); diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index 73c1209733b5f..d8c93f66aa910 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -1,5 +1,6 @@ import { Lazy, Stack, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; +import { IAlarmAction } from './alarm-action'; import { AlarmBase, IAlarm } from './alarm-base'; import { CfnAlarm, CfnAlarmProps } from './cloudwatch.generated'; import { HorizontalAnnotation } from './graph'; @@ -224,6 +225,33 @@ export class Alarm extends AlarmBase { return this.annotation; } + /** + * Trigger this action if the alarm fires + * + * Typically the ARN of an SNS topic or ARN of an AutoScaling policy. + */ + public addAlarmAction(...actions: IAlarmAction[]) { + if (this.alarmActionArns === undefined) { + this.alarmActionArns = []; + } + + this.alarmActionArns.push(...actions.map(a => + this.validateActionArn(a.bind(this, this).alarmActionArn), + )); + } + + private validateActionArn(actionArn: string): string { + const ec2ActionsRegexp: RegExp = /arn:aws:automate:[a-z|\d|-]+:ec2:[a-z]+/; + if (ec2ActionsRegexp.test(actionArn)) { + // Check per-instance metric + const metricConfig = this.metric.toMetricConfig(); + if (metricConfig.metricStat?.dimensions?.length != 1 || metricConfig.metricStat?.dimensions![0].name != 'InstanceId') { + throw new Error(`EC2 alarm actions requires an EC2 Per-Instance Metric. (${JSON.stringify(metricConfig)} does not have an 'InstanceId' dimension)`); + } + } + return actionArn; + } + private renderMetric(metric: IMetric) { const self = this; return dispatchMetric(metric, { diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts index c7c0f647c2e58..c6a727c023c16 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts @@ -42,6 +42,20 @@ export = { test.done(); }, + 'non ec2 instance related alarm does not accept EC2 action'(test: Test) { + + const stack = new Stack(); + const alarm = new Alarm(stack, 'Alarm', { + metric: testMetric, + threshold: 1000, + evaluationPeriods: 2, + }); + + test.throws(() => { + alarm.addAlarmAction(new Ec2TestAlarmAction('arn:aws:automate:us-east-1:ec2:reboot')); + }, /EC2 alarm actions requires an EC2 Per-Instance Metric. \(.+ does not have an 'InstanceId' dimension\)/); + test.done(); + }, 'can make simple alarm'(test: Test) { // GIVEN const stack = new Stack(); @@ -253,3 +267,12 @@ class TestAlarmAction implements IAlarmAction { return { alarmActionArn: this.arn }; } } + +class Ec2TestAlarmAction implements IAlarmAction { + constructor(private readonly arn: string) { + } + + public bind(_scope: Construct, _alarm: IAlarm) { + return { alarmActionArn: this.arn }; + } +} diff --git a/packages/@aws-cdk/aws-codebuild/README.md b/packages/@aws-cdk/aws-codebuild/README.md index 9f35a81656b56..cdc3d9f59b665 100644 --- a/packages/@aws-cdk/aws-codebuild/README.md +++ b/packages/@aws-cdk/aws-codebuild/README.md @@ -617,3 +617,31 @@ if (project.enableBatchBuilds()) { console.log('Batch builds were enabled'); } ``` + +## Timeouts + +There are two types of timeouts that can be set when creating your Project. +The `timeout` property can be used to set an upper limit on how long your Project is able to run without being marked as completed. +The default is 60 minutes. +An example of overriding the default follows. + +```ts +import * as codebuild from '@aws-cdk/aws-codebuild'; + +new codebuild.Project(stack, 'MyProject', { + timeout: Duration.minutes(90) +}); +``` + +The `queuedTimeout` property can be used to set an upper limit on how your Project remains queued to run. +There is no default value for this property. +As an example, to allow your Project to queue for up to thirty (30) minutes before the build fails, +use the following code. + +```ts +import * as codebuild from '@aws-cdk/aws-codebuild'; + +new codebuild.Project(stack, 'MyProject', { + queuedTimeout: Duration.minutes(30) +}); +``` diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index 2f31bc897f9f8..9477f5be3a246 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -575,6 +575,15 @@ export interface CommonProjectProps { * @default - no log configuration is set */ readonly logging?: LoggingOptions; + + /** + * The number of minutes after which AWS CodeBuild stops the build if it's + * still in queue. For valid values, see the timeoutInMinutes field in the AWS + * CodeBuild User Guide. + * + * @default - no queue timeout is set + */ + readonly queuedTimeout?: Duration } export interface ProjectProps extends CommonProjectProps { @@ -869,6 +878,7 @@ export class Project extends ProjectBase { cache: cache._toCloudFormation(), name: this.physicalName, timeoutInMinutes: props.timeout && props.timeout.toMinutes(), + queuedTimeoutInMinutes: props.queuedTimeout && props.queuedTimeout.toMinutes(), secondarySources: Lazy.any({ produce: () => this.renderSecondarySources() }), secondarySourceVersions: Lazy.any({ produce: () => this.renderSecondarySourceVersions() }), secondaryArtifacts: Lazy.any({ produce: () => this.renderSecondaryArtifacts() }), diff --git a/packages/@aws-cdk/aws-codebuild/test/test.project.ts b/packages/@aws-cdk/aws-codebuild/test/test.project.ts index 2912d832bd2c0..159511589ead7 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.project.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.project.ts @@ -960,4 +960,47 @@ export = { test.done(); }, }, + + 'Timeouts': { + 'can add queued timeout'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.s3({ + bucket: new s3.Bucket(stack, 'Bucket'), + path: 'path', + }), + queuedTimeout: cdk.Duration.minutes(30), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + QueuedTimeoutInMinutes: 30, + })); + + test.done(); + }, + 'can override build timeout'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.s3({ + bucket: new s3.Bucket(stack, 'Bucket'), + path: 'path', + }), + timeout: cdk.Duration.minutes(30), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + TimeoutInMinutes: 30, + })); + + test.done(); + }, + }, }; diff --git a/packages/@aws-cdk/aws-docdb/lib/cluster.ts b/packages/@aws-cdk/aws-docdb/lib/cluster.ts index 529f446c265b9..f60a332d1b77f 100644 --- a/packages/@aws-cdk/aws-docdb/lib/cluster.ts +++ b/packages/@aws-cdk/aws-docdb/lib/cluster.ts @@ -238,7 +238,7 @@ export class DatabaseCluster extends DatabaseClusterBase { public readonly clusterResourceIdentifier: string; /** - * The connections object to implement IConectable + * The connections object to implement IConnectable */ public readonly connections: ec2.Connections; 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 001e9b369ea83..e2ae4352eabe5 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 @@ -37,6 +37,6 @@ "eslint-plugin-standard": "^4.1.0", "jest": "^26.6.3", "lambda-tester": "^3.6.0", - "nock": "^13.0.9" + "nock": "^13.0.10" } } diff --git a/packages/@aws-cdk/aws-dynamodb-global/lib/global-table-coordinator.ts b/packages/@aws-cdk/aws-dynamodb-global/lib/global-table-coordinator.ts index 0acd9b1cdc3d6..942d48116f06f 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/lib/global-table-coordinator.ts +++ b/packages/@aws-cdk/aws-dynamodb-global/lib/global-table-coordinator.ts @@ -19,7 +19,7 @@ export class GlobalTableCoordinator extends cdk.Stack { code: lambda.Code.fromAsset(path.resolve(__dirname, '../', 'lambda-packages', 'aws-global-table-coordinator', 'lib')), description: 'Lambda to make DynamoDB a global table', handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, timeout: cdk.Duration.minutes(5), uuid: 'D38B65A6-6B54-4FB6-9BAD-9CD40A6DAC12', }); diff --git a/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json b/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json index 6050c1497c918..5b4f51877ccfe 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json +++ b/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json @@ -203,7 +203,7 @@ "Arn" ] }, - "Runtime": "nodejs10.x", + "Runtime": "nodejs14.x", "Description": "Lambda to make DynamoDB a global table", "Timeout": 300 }, diff --git a/packages/@aws-cdk/aws-dynamodb/README.md b/packages/@aws-cdk/aws-dynamodb/README.md index acd3f1820c156..ac540dd11670c 100644 --- a/packages/@aws-cdk/aws-dynamodb/README.md +++ b/packages/@aws-cdk/aws-dynamodb/README.md @@ -109,6 +109,17 @@ globalTable.autoScaleWriteCapacity({ }).scaleOnUtilization({ targetUtilizationPercent: 75 }); ``` +When adding a replica region for a large table, you might want to increase the +timeout for the replication operation: + +```ts +const globalTable = new dynamodb.Table(this, 'Table', { + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + replicationRegions: ['us-east-1', 'us-east-2', 'us-west-2'], + replicationTimeout: Duration.hours(2), // defaults to Duration.minutes(30) +}); +``` + ## Encryption All user data stored in Amazon DynamoDB is fully encrypted at rest. When creating a new table, you can choose to encrypt using the following customer master keys (CMK) to encrypt your table: diff --git a/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts b/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts index 814bad346ece2..1554dcc84004d 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts @@ -5,27 +5,34 @@ import { DynamoDB } from 'aws-sdk'; // eslint-disable-line import/no-extraneous- export async function onEventHandler(event: OnEventRequest): Promise { console.log('Event: %j', event); - /** - * Process only Create and Delete requests. We shouldn't receive any - * update request and in case we do there is nothing to update. - */ + const dynamodb = new DynamoDB(); + + let updateTableAction: 'Create' | 'Update' | 'Delete'; if (event.RequestType === 'Create' || event.RequestType === 'Delete') { - const dynamodb = new DynamoDB(); - - const data = await dynamodb.updateTable({ - TableName: event.ResourceProperties.TableName, - ReplicaUpdates: [ - { - [event.RequestType]: { - RegionName: event.ResourceProperties.Region, - }, - }, - ], - }).promise(); - console.log('Update table: %j', data); + updateTableAction = event.RequestType; + } else { // Update + // This can only be a table replacement so we create a replica + // in the new table. The replica for the "old" table will be + // deleted when CF issues a Delete event on the old physical + // resource id. + updateTableAction = 'Create'; } - return { PhysicalResourceId: event.ResourceProperties.Region }; + const data = await dynamodb.updateTable({ + TableName: event.ResourceProperties.TableName, + ReplicaUpdates: [ + { + [updateTableAction]: { + RegionName: event.ResourceProperties.Region, + }, + }, + ], + }).promise(); + console.log('Update table: %j', data); + + return event.RequestType === 'Create' || event.RequestType === 'Update' + ? { PhysicalResourceId: `${event.ResourceProperties.TableName}-${event.ResourceProperties.Region}` } + : {}; } export async function isCompleteHandler(event: IsCompleteRequest): Promise { diff --git a/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts b/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts index 718c0e693e454..d984c4bb7b3ff 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts @@ -9,14 +9,26 @@ import { Construct } from 'constructs'; // eslint-disable-next-line no-duplicate-imports, import/order import { Construct as CoreConstruct } from '@aws-cdk/core'; +/** + * Properties for a ReplicaProvider + */ +export interface ReplicaProviderProps { + /** + * The timeout for the replication operation. + * + * @default Duration.minutes(30) + */ + readonly timeout?: Duration; +} + export class ReplicaProvider extends NestedStack { /** * Creates a stack-singleton resource provider nested stack. */ - public static getOrCreate(scope: Construct) { + public static getOrCreate(scope: Construct, props: ReplicaProviderProps = {}) { const stack = Stack.of(scope); const uid = '@aws-cdk/aws-dynamodb.ReplicaProvider'; - return stack.node.tryFindChild(uid) as ReplicaProvider || new ReplicaProvider(stack, uid); + return stack.node.tryFindChild(uid) as ReplicaProvider ?? new ReplicaProvider(stack, uid, props); } /** @@ -34,7 +46,7 @@ export class ReplicaProvider extends NestedStack { */ public readonly isCompleteHandler: lambda.Function; - private constructor(scope: Construct, id: string) { + private constructor(scope: Construct, id: string, props: ReplicaProviderProps = {}) { super(scope as CoreConstruct, id); const code = lambda.Code.fromAsset(path.join(__dirname, 'replica-handler')); @@ -80,6 +92,7 @@ export class ReplicaProvider extends NestedStack { onEventHandler: this.onEventHandler, isCompleteHandler: this.isCompleteHandler, queryInterval: Duration.seconds(10), + totalTimeout: props.timeout, }); } } diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 1b12eef42de1f..9334192f5610d 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -3,8 +3,8 @@ 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, CustomResource, Fn, - IResource, Lazy, Names, RemovalPolicy, Resource, Stack, Token, + Aws, CfnCondition, CfnCustomResource, CustomResource, Duration, + Fn, IResource, Lazy, Names, RemovalPolicy, Resource, Stack, Token, } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { DynamoDBMetrics } from './dynamodb-canned-metrics.generated'; @@ -218,6 +218,13 @@ export interface TableOptions { * @experimental */ readonly replicationRegions?: string[]; + + /** + * The timeout for a table replication operation in a single region. + * + * @default Duration.minutes(30) + */ + readonly replicationTimeout?: Duration; } /** @@ -1135,7 +1142,7 @@ export class Table extends TableBase { } if (props.replicationRegions && props.replicationRegions.length > 0) { - this.createReplicaTables(props.replicationRegions); + this.createReplicaTables(props.replicationRegions, props.replicationTimeout); } } @@ -1451,14 +1458,14 @@ export class Table extends TableBase { * * @param regions regions where to create tables */ - private createReplicaTables(regions: string[]) { + private createReplicaTables(regions: string[], timeout?: Duration) { const stack = Stack.of(this); if (!Token.isUnresolved(stack.region) && regions.includes(stack.region)) { throw new Error('`replicationRegions` cannot include the region where this stack is deployed.'); } - const provider = ReplicaProvider.getOrCreate(this); + const provider = ReplicaProvider.getOrCreate(this, { timeout }); // Documentation at https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/V2gt_IAM.html // is currently incorrect. AWS Support recommends `dynamodb:*` in both source and destination regions @@ -1670,12 +1677,19 @@ interface ScalableAttributePair { */ class SourceTableAttachedPolicy extends CoreConstruct implements iam.IGrantable { public readonly grantPrincipal: iam.IPrincipal; - public readonly policy: iam.IPolicy; + public readonly policy: iam.IManagedPolicy; public constructor(sourceTable: Table, role: iam.IRole) { - super(sourceTable, `SourceTableAttachedPolicy-${Names.nodeUniqueId(role.node)}`); - - const policy = new iam.Policy(this, 'Resource', { roles: [role] }); + super(sourceTable, `SourceTableAttachedManagedPolicy-${Names.nodeUniqueId(role.node)}`); + + const policy = new iam.ManagedPolicy(this, 'Resource', { + // A CF update of the description property of a managed policy requires + // a replacement. Use the table name in the description to force a managed + // policy replacement when the table name changes. This way we preserve permissions + // to delete old replicas in case of a table replacement. + description: `DynamoDB replication managed policy for table ${sourceTable.tableName}`, + roles: [role], + }); this.policy = policy; this.grantPrincipal = new SourceTableAttachedPrincipal(role, policy); } @@ -1686,7 +1700,7 @@ class SourceTableAttachedPolicy extends CoreConstruct implements iam.IGrantable * `SourceTableAttachedPolicy` class so it can act as an `IGrantable`. */ class SourceTableAttachedPrincipal extends iam.PrincipalBase { - public constructor(private readonly role: iam.IRole, private readonly policy: iam.Policy) { + public constructor(private readonly role: iam.IRole, private readonly policy: iam.ManagedPolicy) { super(); } diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index 5737a58a67816..01ce9c576c096 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -81,7 +81,7 @@ "jest": "^26.6.3", "pkglint": "0.0.0", "sinon": "^9.2.4", - "ts-jest": "^26.5.2" + "ts-jest": "^26.5.3" }, "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 768094c253213..cddba2ff1504b 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -4,6 +4,7 @@ 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, Aws, CfnDeletionPolicy, ConstructNode, Duration, PhysicalName, RemovalPolicy, Resource, Stack, Tags } from '@aws-cdk/core'; +import * as cr from '@aws-cdk/custom-resources'; import { testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import { Construct } from 'constructs'; import { @@ -20,6 +21,8 @@ import { CfnTable, } from '../lib'; +jest.mock('@aws-cdk/custom-resources'); + /* eslint-disable quote-props */ // CDK parameters @@ -2295,12 +2298,6 @@ describe('global', () => { // THEN expect(stack).toHaveResource('Custom::DynamoDBReplica', { Properties: { - ServiceToken: { - 'Fn::GetAtt': [ - 'awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D', - 'Outputs.awscdkawsdynamodbReplicaProviderframeworkonEventF9504691Arn', - ], - }, TableName: { Ref: 'TableCD117FA1', }, @@ -2311,12 +2308,6 @@ describe('global', () => { expect(stack).toHaveResource('Custom::DynamoDBReplica', { Properties: { - ServiceToken: { - 'Fn::GetAtt': [ - 'awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D', - 'Outputs.awscdkawsdynamodbReplicaProviderframeworkonEventF9504691Arn', - ], - }, TableName: { Ref: 'TableCD117FA1', }, @@ -2814,6 +2805,26 @@ describe('global', () => { // THEN expect(SynthUtils.toCloudFormation(stack).Conditions).toBeUndefined(); }); + + test('can configure timeout', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new Table(stack, 'Table', { + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, + replicationRegions: ['eu-central-1'], + replicationTimeout: Duration.hours(1), + }); + + // THEN + expect(cr.Provider).toHaveBeenCalledWith(expect.anything(), expect.any(String), expect.objectContaining({ + totalTimeout: Duration.hours(1), + })); + }); }); test('L1 inside L2 expects removalpolicy to have been set', () => { diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json index 89a9c3807fc21..b4ea44f2709c4 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json @@ -26,8 +26,8 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF": { - "Type": "AWS::IAM::Policy", + "TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B771F8F2CCB": { + "Type": "AWS::IAM::ManagedPolicy", "Properties": { "PolicyDocument": { "Statement": [ @@ -93,7 +93,18 @@ ], "Version": "2012-10-17" }, - "PolicyName": "leAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF", + "Description": { + "Fn::Join": [ + "", + [ + "DynamoDB replication managed policy for table ", + { + "Ref": "TableCD117FA1" + } + ] + ] + }, + "Path": "/", "Roles": [ { "Fn::GetAtt": [ @@ -104,8 +115,8 @@ ] } }, - "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D": { - "Type": "AWS::IAM::Policy", + "TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1A5DC546D2": { + "Type": "AWS::IAM::ManagedPolicy", "Properties": { "PolicyDocument": { "Statement": [ @@ -127,7 +138,18 @@ ], "Version": "2012-10-17" }, - "PolicyName": "ttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D", + "Description": { + "Fn::Join": [ + "", + [ + "DynamoDB replication managed policy for table ", + { + "Ref": "TableCD117FA1" + } + ] + ] + }, + "Path": "/", "Roles": [ { "Fn::GetAtt": [ @@ -153,8 +175,8 @@ "Region": "us-east-2" }, "DependsOn": [ - "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D", - "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF", + "TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1A5DC546D2", + "TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B771F8F2CCB", "TableWriteScalingTargetE5669214", "TableWriteScalingTargetTrackingD78DCCD8" ], @@ -178,8 +200,8 @@ }, "DependsOn": [ "TableReplicauseast28A15C236", - "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1AE3D3CF6D", - "TableSourceTableAttachedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B77945CD5DF", + "TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRoleBE2B1C1A5DC546D2", + "TableSourceTableAttachedManagedPolicyawscdkdynamodbglobalreplicasprovisionedawscdkawsdynamodbReplicaProviderOnEventHandlerServiceRoleD9856B771F8F2CCB", "TableWriteScalingTargetE5669214", "TableWriteScalingTargetTrackingD78DCCD8" ], @@ -256,7 +278,7 @@ }, "/", { - "Ref": "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3BucketEDAACFE7" + "Ref": "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3Bucket806FEB2C" }, "/", { @@ -266,7 +288,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3VersionKey6FF3D50F" + "Ref": "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3VersionKey81C7BC5B" } ] } @@ -279,7 +301,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3VersionKey6FF3D50F" + "Ref": "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3VersionKey81C7BC5B" } ] } @@ -289,11 +311,11 @@ ] }, "Parameters": { - "referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket50997EC4Ref": { - "Ref": "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket1C6779E0" + "referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3BucketD1258B42Ref": { + "Ref": "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3BucketDEBF01E6" }, - "referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey0F47C425Ref": { - "Ref": "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey5C1D9275" + "referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3VersionKey0F5C355ERef": { + "Ref": "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3VersionKey42EBA2AE" }, "referencetoawscdkdynamodbglobalreplicasprovisionedAssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3Bucket6C51C355Ref": { "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" @@ -334,17 +356,17 @@ } }, "Parameters": { - "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket1C6779E0": { + "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3BucketDEBF01E6": { "Type": "String", - "Description": "S3 bucket for asset \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + "Description": "S3 bucket for asset \"dd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776\"" }, - "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey5C1D9275": { + "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3VersionKey42EBA2AE": { "Type": "String", - "Description": "S3 key for asset version \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + "Description": "S3 key for asset version \"dd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776\"" }, - "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714ArtifactHash477AAEA7": { + "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776ArtifactHash692B4CCE": { "Type": "String", - "Description": "Artifact hash for asset \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + "Description": "Artifact hash for asset \"dd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776\"" }, "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1": { "Type": "String", @@ -358,17 +380,17 @@ "Type": "String", "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" }, - "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3BucketEDAACFE7": { + "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3Bucket806FEB2C": { "Type": "String", - "Description": "S3 bucket for asset \"e31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fd\"" + "Description": "S3 bucket for asset \"d56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aad\"" }, - "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdS3VersionKey6FF3D50F": { + "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3VersionKey81C7BC5B": { "Type": "String", - "Description": "S3 key for asset version \"e31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fd\"" + "Description": "S3 key for asset version \"d56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aad\"" }, - "AssetParameterse31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fdArtifactHash898696F1": { + "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadArtifactHashD0230F6F": { "Type": "String", - "Description": "Artifact hash for asset \"e31d108faccc52dcd9a9d86276a05e6ad861311925fe6931eadc31d0fe17e1fd\"" + "Description": "Artifact hash for asset \"d56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aad\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json index a66dd3d965ed9..3896ac3a355b2 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json @@ -41,8 +41,8 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA": { - "Type": "AWS::IAM::Policy", + "TableSourceTableAttachedManagedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4A23250B4C": { + "Type": "AWS::IAM::ManagedPolicy", "Properties": { "PolicyDocument": { "Statement": [ @@ -119,7 +119,18 @@ ], "Version": "2012-10-17" }, - "PolicyName": "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA", + "Description": { + "Fn::Join": [ + "", + [ + "DynamoDB replication managed policy for table ", + { + "Ref": "TableCD117FA1" + } + ] + ] + }, + "Path": "/", "Roles": [ { "Fn::GetAtt": [ @@ -130,8 +141,8 @@ ] } }, - "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA": { - "Type": "AWS::IAM::Policy", + "TableSourceTableAttachedManagedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole3971612857304880": { + "Type": "AWS::IAM::ManagedPolicy", "Properties": { "PolicyDocument": { "Statement": [ @@ -164,7 +175,18 @@ ], "Version": "2012-10-17" }, - "PolicyName": "leSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", + "Description": { + "Fn::Join": [ + "", + [ + "DynamoDB replication managed policy for table ", + { + "Ref": "TableCD117FA1" + } + ] + ] + }, + "Path": "/", "Roles": [ { "Fn::GetAtt": [ @@ -190,8 +212,8 @@ "Region": "eu-west-2" }, "DependsOn": [ - "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", - "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA" + "TableSourceTableAttachedManagedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole3971612857304880", + "TableSourceTableAttachedManagedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4A23250B4C" ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -212,8 +234,8 @@ }, "DependsOn": [ "TableReplicaeuwest290D3CD3A", - "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", - "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA" + "TableSourceTableAttachedManagedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole3971612857304880", + "TableSourceTableAttachedManagedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4A23250B4C" ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -231,7 +253,7 @@ }, "/", { - "Ref": "AssetParametersf8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429ceaS3Bucket434BDB62" + "Ref": "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3Bucket8BB0CECD" }, "/", { @@ -241,7 +263,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersf8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429ceaS3VersionKey01638790" + "Ref": "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3VersionKeyC531296D" } ] } @@ -254,7 +276,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersf8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429ceaS3VersionKey01638790" + "Ref": "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3VersionKeyC531296D" } ] } @@ -264,11 +286,11 @@ ] }, "Parameters": { - "referencetocdkdynamodbglobal20191121AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket71E24D5BRef": { - "Ref": "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket1C6779E0" + "referencetocdkdynamodbglobal20191121AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3Bucket06999F76Ref": { + "Ref": "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3BucketDEBF01E6" }, - "referencetocdkdynamodbglobal20191121AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKeyD88E8BACRef": { - "Ref": "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey5C1D9275" + "referencetocdkdynamodbglobal20191121AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3VersionKey3D988AD7Ref": { + "Ref": "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3VersionKey42EBA2AE" }, "referencetocdkdynamodbglobal20191121AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketC7F3A147Ref": { "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" @@ -283,17 +305,17 @@ } }, "Parameters": { - "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3Bucket1C6779E0": { + "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3BucketDEBF01E6": { "Type": "String", - "Description": "S3 bucket for asset \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + "Description": "S3 bucket for asset \"dd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776\"" }, - "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714S3VersionKey5C1D9275": { + "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776S3VersionKey42EBA2AE": { "Type": "String", - "Description": "S3 key for asset version \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + "Description": "S3 key for asset version \"dd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776\"" }, - "AssetParametersf13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714ArtifactHash477AAEA7": { + "AssetParametersdd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776ArtifactHash692B4CCE": { "Type": "String", - "Description": "Artifact hash for asset \"f13d472270faaa08099009152a8848a0e7434b14773f3c3f94acca6f6c3ae714\"" + "Description": "Artifact hash for asset \"dd0a4ac30ffa331e472caec08a7784ac440d122a6f924b1bea7d48dc85f8f776\"" }, "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1": { "Type": "String", @@ -307,17 +329,17 @@ "Type": "String", "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" }, - "AssetParametersf8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429ceaS3Bucket434BDB62": { + "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3Bucket8BB0CECD": { "Type": "String", - "Description": "S3 bucket for asset \"f8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429cea\"" + "Description": "S3 bucket for asset \"a789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4\"" }, - "AssetParametersf8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429ceaS3VersionKey01638790": { + "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3VersionKeyC531296D": { "Type": "String", - "Description": "S3 key for asset version \"f8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429cea\"" + "Description": "S3 key for asset version \"a789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4\"" }, - "AssetParametersf8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429ceaArtifactHashD0E61C22": { + "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4ArtifactHash9D92B407": { "Type": "String", - "Description": "Artifact hash for asset \"f8cfc24954f0c95960d9a93888c01bf5e95802f26bfa5dc6fde5c913a1429cea\"" + "Description": "Artifact hash for asset \"a789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-dynamodb/test/replica-provider.test.ts b/packages/@aws-cdk/aws-dynamodb/test/replica-provider.test.ts index 3a1d97bd4b345..4b5acef3d15cb 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/replica-provider.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/replica-provider.test.ts @@ -54,25 +54,62 @@ test('on event', async () => { }); expect(data).toEqual({ - PhysicalResourceId: 'eu-west-2', + PhysicalResourceId: 'my-table-eu-west-2', }); }); -test('on event does not call updateTable for Update requests', async () => { +test('on event calls updateTable with Create for Update requests with table replacement', async () => { const updateTableMock = sinon.fake.resolves({}); AWS.mock('DynamoDB', 'updateTable', updateTableMock); const data = await onEventHandler({ ...createEvent, + OldResourceProperties: { + TableName: 'my-old-table', + }, RequestType: 'Update', }); - sinon.assert.notCalled(updateTableMock); + sinon.assert.calledWith(updateTableMock, { + TableName: 'my-table', + ReplicaUpdates: [ + { + Create: { + RegionName: 'eu-west-2', + }, + }, + ], + }); expect(data).toEqual({ - PhysicalResourceId: 'eu-west-2', + PhysicalResourceId: 'my-table-eu-west-2', + }); +}); + +test('on event calls updateTable with Delete', async () => { + const updateTableMock = sinon.fake.resolves({}); + + AWS.mock('DynamoDB', 'updateTable', updateTableMock); + + const data = await onEventHandler({ + ...createEvent, + RequestType: 'Delete', + }); + + sinon.assert.calledWith(updateTableMock, { + TableName: 'my-table', + ReplicaUpdates: [ + { + Delete: { + RegionName: 'eu-west-2', + }, + }, + ], }); + + // Physical resource id never changed on Delete + expect(data).toEqual({}); }); test('is complete for create returns false without replicas', async () => { diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 49fff4b5c4f63..90b5b4be9cfb8 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -981,6 +981,51 @@ instance.userData.addExecuteFileCommand({ asset.grantRead( instance.role ); ``` +### Multipart user data + +In addition, to above the `MultipartUserData` can be used to change instance startup behavior. Multipart user data are composed +from separate parts forming archive. The most common parts are scripts executed during instance set-up. However, there are other +kinds, too. + +The advantage of multipart archive is in flexibility when it's needed to add additional parts or to use specialized parts to +fine tune instance startup. Some services (like AWS Batch) supports only `MultipartUserData`. + +The parts can be executed at different moment of instance start-up and can serve a different purposes. This is controlled by `contentType` property. +For common scripts, `text/x-shellscript; charset="utf-8"` can be used as content type. + +In order to create archive the `MultipartUserData` has to be instantiated. Than, user can add parts to multipart archive using `addPart`. The `MultipartBody` contains methods supporting creation of body parts. + +If the very custom part is required, it can be created using `MultipartUserData.fromRawBody`, in this case full control over content type, +transfer encoding, and body properties is given to the user. + +Below is an example for creating multipart user data with single body part responsible for installing `awscli` and configuring maximum size +of storage used by Docker containers: + +```ts +const bootHookConf = ec2.UserData.forLinux(); +bootHookConf.addCommands('cloud-init-per once docker_options echo \'OPTIONS="${OPTIONS} --storage-opt dm.basesize=40G"\' >> /etc/sysconfig/docker'); + +const setupCommands = ec2.UserData.forLinux(); +setupCommands.addCommands('sudo yum install awscli && echo Packages installed らと > /var/tmp/setup'); + +const multipartUserData = new ec2.MultipartUserData(); +// The docker has to be configured at early stage, so content type is overridden to boothook +multipartUserData.addPart(ec2.MultipartBody.fromUserData(bootHookConf, 'text/cloud-boothook; charset="us-ascii"')); +// Execute the rest of setup +multipartUserData.addPart(ec2.MultipartBody.fromUserData(setupCommands)); + +new ec2.LaunchTemplate(stack, '', { + userData: multipartUserData, + blockDevices: [ + // Block device configuration rest + ] +}); +``` + +For more information see +[Specifying Multiple User Data Blocks Using a MIME Multi Part Archive](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/bootstrap_container_instance.html#multi-part_user_data) + + ## Importing existing subnet To import an existing Subnet, call `Subnet.fromSubnetAttributes()` or diff --git a/packages/@aws-cdk/aws-ec2/lib/nat.ts b/packages/@aws-cdk/aws-ec2/lib/nat.ts index c164362703692..bf65671b92287 100644 --- a/packages/@aws-cdk/aws-ec2/lib/nat.ts +++ b/packages/@aws-cdk/aws-ec2/lib/nat.ts @@ -7,6 +7,26 @@ import { Port } from './port'; import { ISecurityGroup, SecurityGroup } from './security-group'; import { PrivateSubnet, PublicSubnet, RouterType, Vpc } from './vpc'; +/** + * Direction of traffic to allow all by default. + */ +export enum NatTrafficDirection { + /** + * Allow all outbound traffic and disallow all inbound traffic. + */ + OUTBOUND_ONLY = 'OUTBOUND_ONLY', + + /** + * Allow all outbound and inbound traffic. + */ + INBOUND_AND_OUTBOUND = 'INBOUND_AND_OUTBOUND', + + /** + * Disallow all outbound and inbound traffic. + */ + NONE = 'NONE', +} + /** * Pair represents a gateway created by NAT Provider */ @@ -148,7 +168,7 @@ export interface NatInstanceProps { readonly securityGroup?: ISecurityGroup; /** - * Allow all traffic through the NAT instance + * Allow all inbound traffic through the NAT instance * * If you set this to false, you must configure the NAT instance's security * groups in another way, either by passing in a fully configured Security @@ -157,8 +177,24 @@ export interface NatInstanceProps { * Provider to a Vpc. * * @default true + * @deprecated - Use `defaultAllowedTraffic`. */ readonly allowAllTraffic?: boolean; + + /** + * Direction to allow all traffic through the NAT instance by default. + * + * By default, inbound and outbound traffic is allowed. + * + * If you set this to another value than INBOUND_AND_OUTBOUND, you must + * configure the NAT instance's security groups in another way, either by + * passing in a fully configured Security Group using the `securityGroup` + * property, or by configuring it using the `.securityGroup` or + * `.connections` members after passing the NAT Instance Provider to a Vpc. + * + * @default NatTrafficDirection.INBOUND_AND_OUTBOUND + */ + readonly defaultAllowedTraffic?: NatTrafficDirection; } /** @@ -205,18 +241,26 @@ export class NatInstanceProvider extends NatProvider implements IConnectable { constructor(private readonly props: NatInstanceProps) { super(); + + if (props.defaultAllowedTraffic !== undefined && props.allowAllTraffic !== undefined) { + throw new Error('Can not specify both of \'defaultAllowedTraffic\' and \'defaultAllowedTraffic\'; prefer \'defaultAllowedTraffic\''); + } } public configureNat(options: ConfigureNatOptions) { + const defaultDirection = this.props.defaultAllowedTraffic ?? + (this.props.allowAllTraffic ?? true ? NatTrafficDirection.INBOUND_AND_OUTBOUND : NatTrafficDirection.OUTBOUND_ONLY); + // Create the NAT instances. They can share a security group and a Role. const machineImage = this.props.machineImage || new NatInstanceImage(); this._securityGroup = this.props.securityGroup ?? new SecurityGroup(options.vpc, 'NatSecurityGroup', { vpc: options.vpc, description: 'Security Group for NAT instances', + allowAllOutbound: isOutboundAllowed(defaultDirection), }); this._connections = new Connections({ securityGroups: [this._securityGroup] }); - if (this.props.allowAllTraffic ?? true) { + if (isInboundAllowed(defaultDirection)) { this.connections.allowFromAnyIpv4(Port.allTraffic()); } @@ -325,3 +369,12 @@ export class NatInstanceImage extends LookupMachineImage { }); } } + +function isOutboundAllowed(direction: NatTrafficDirection) { + return direction === NatTrafficDirection.INBOUND_AND_OUTBOUND || + direction === NatTrafficDirection.OUTBOUND_ONLY; +} + +function isInboundAllowed(direction: NatTrafficDirection) { + return direction === NatTrafficDirection.INBOUND_AND_OUTBOUND; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/lib/port.ts b/packages/@aws-cdk/aws-ec2/lib/port.ts index df8d671271b85..314c8d615b0dd 100644 --- a/packages/@aws-cdk/aws-ec2/lib/port.ts +++ b/packages/@aws-cdk/aws-ec2/lib/port.ts @@ -9,6 +9,8 @@ export enum Protocol { UDP = 'udp', ICMP = 'icmp', ICMPV6 = '58', + ESP = 'esp', + AH = 'ah', } /** @@ -171,6 +173,30 @@ export class Port { }); } + /** + * A single ESP port + */ + public static esp(): Port { + return new Port({ + protocol: Protocol.ESP, + fromPort: 50, + toPort: 50, + stringRepresentation: 'ESP 50', + }); + } + + /** + * A single AH port + */ + public static ah(): Port { + return new Port({ + protocol: Protocol.AH, + fromPort: 51, + toPort: 51, + stringRepresentation: 'AH 51', + }); + } + /** * Whether the rule containing this port range can be inlined into a securitygroup or not. */ diff --git a/packages/@aws-cdk/aws-ec2/lib/user-data.ts b/packages/@aws-cdk/aws-ec2/lib/user-data.ts index 20061bd609636..418b6d671846d 100644 --- a/packages/@aws-cdk/aws-ec2/lib/user-data.ts +++ b/packages/@aws-cdk/aws-ec2/lib/user-data.ts @@ -1,5 +1,5 @@ import { IBucket } from '@aws-cdk/aws-s3'; -import { CfnElement, Resource, Stack } from '@aws-cdk/core'; +import { CfnElement, Fn, Resource, Stack } from '@aws-cdk/core'; import { OperatingSystemType } from './machine-image'; /** @@ -276,3 +276,257 @@ class CustomUserData extends UserData { throw new Error('CustomUserData does not support addSignalOnExitCommand, use UserData.forLinux() or UserData.forWindows() instead.'); } } + +/** + * Options when creating `MultipartBody`. + */ +export interface MultipartBodyOptions { + + /** + * `Content-Type` header of this part. + * + * Some examples of content types: + * * `text/x-shellscript; charset="utf-8"` (shell script) + * * `text/cloud-boothook; charset="utf-8"` (shell script executed during boot phase) + * + * For Linux shell scripts use `text/x-shellscript`. + */ + readonly contentType: string; + + /** + * `Content-Transfer-Encoding` header specifying part encoding. + * + * @default undefined - body is not encoded + */ + readonly transferEncoding?: string; + + /** + * The body of message. + * + * @default undefined - body will not be added to part + */ + readonly body?: string, +} + +/** + * The base class for all classes which can be used as {@link MultipartUserData}. + */ +export abstract class MultipartBody { + /** + * Content type for shell scripts + */ + public static readonly SHELL_SCRIPT = 'text/x-shellscript; charset="utf-8"'; + + /** + * Content type for boot hooks + */ + public static readonly CLOUD_BOOTHOOK = 'text/cloud-boothook; charset="utf-8"'; + + /** + * Constructs the new `MultipartBody` wrapping existing `UserData`. Modification to `UserData` are reflected + * in subsequent renders of the part. + * + * For more information about content types see {@link MultipartBodyOptions.contentType}. + * + * @param userData user data to wrap into body part + * @param contentType optional content type, if default one should not be used + */ + public static fromUserData(userData: UserData, contentType?: string): MultipartBody { + return new MultipartBodyUserDataWrapper(userData, contentType); + } + + /** + * Constructs the raw `MultipartBody` using specified body, content type and transfer encoding. + * + * When transfer encoding is specified (typically as Base64), it's caller responsibility to convert body to + * Base64 either by wrapping with `Fn.base64` or by converting it by other converters. + */ + public static fromRawBody(opts: MultipartBodyOptions): MultipartBody { + return new MultipartBodyRaw(opts); + } + + public constructor() { + } + + /** + * Render body part as the string. + * + * Subclasses should not add leading nor trailing new line characters (\r \n) + */ + public abstract renderBodyPart(): string[]; +} + +/** + * The raw part of multi-part user data, which can be added to {@link MultipartUserData}. + */ +class MultipartBodyRaw extends MultipartBody { + public constructor(private readonly props: MultipartBodyOptions) { + super(); + } + + /** + * Render body part as the string. + */ + public renderBodyPart(): string[] { + const result: string[] = []; + + result.push(`Content-Type: ${this.props.contentType}`); + + if (this.props.transferEncoding != null) { + result.push(`Content-Transfer-Encoding: ${this.props.transferEncoding}`); + } + // One line free after separator + result.push(''); + + if (this.props.body != null) { + result.push(this.props.body); + // The new line added after join will be consumed by encapsulating or closing boundary + } + + return result; + } +} + +/** + * Wrapper for `UserData`. + */ +class MultipartBodyUserDataWrapper extends MultipartBody { + private readonly contentType: string; + + public constructor(private readonly userData: UserData, contentType?: string) { + super(); + + this.contentType = contentType || MultipartBody.SHELL_SCRIPT; + } + + /** + * Render body part as the string. + */ + public renderBodyPart(): string[] { + const result: string[] = []; + + result.push(`Content-Type: ${this.contentType}`); + result.push('Content-Transfer-Encoding: base64'); + result.push(''); + result.push(Fn.base64(this.userData.render())); + + return result; + } +} + +/** + * Options for creating {@link MultipartUserData} + */ +export interface MultipartUserDataOptions { + /** + * The string used to separate parts in multipart user data archive (it's like MIME boundary). + * + * This string should contain [a-zA-Z0-9()+,-./:=?] characters only, and should not be present in any part, or in text content of archive. + * + * @default `+AWS+CDK+User+Data+Separator==` + */ + readonly partsSeparator?: string; +} + +/** + * Mime multipart user data. + * + * This class represents MIME multipart user data, as described in. + * [Specifying Multiple User Data Blocks Using a MIME Multi Part Archive](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/bootstrap_container_instance.html#multi-part_user_data) + * + */ +export class MultipartUserData extends UserData { + private static readonly USE_PART_ERROR = 'MultipartUserData does not support this operation. Please add part using addPart.'; + private static readonly BOUNDRY_PATTERN = '[^a-zA-Z0-9()+,-./:=?]'; + + private parts: MultipartBody[] = []; + + private opts: MultipartUserDataOptions; + + constructor(opts?: MultipartUserDataOptions) { + super(); + + let partsSeparator: string; + + // Validate separator + if (opts?.partsSeparator != null) { + if (new RegExp(MultipartUserData.BOUNDRY_PATTERN).test(opts!.partsSeparator)) { + throw new Error(`Invalid characters in separator. Separator has to match pattern ${MultipartUserData.BOUNDRY_PATTERN}`); + } else { + partsSeparator = opts!.partsSeparator; + } + } else { + partsSeparator = '+AWS+CDK+User+Data+Separator=='; + } + + this.opts = { + partsSeparator: partsSeparator, + }; + } + + /** + * Adds a part to the list of parts. + */ + public addPart(part: MultipartBody) { + this.parts.push(part); + } + + /** + * Adds a multipart part based on a UserData object + * + * This is the same as calling: + * + * ```ts + * multiPart.addPart(MultipartBody.fromUserData(userData, contentType)); + * ``` + */ + public addUserDataPart(userData: UserData, contentType?: string) { + this.addPart(MultipartBody.fromUserData(userData, contentType)); + } + + public render(): string { + const boundary = this.opts.partsSeparator; + // Now build final MIME archive - there are few changes from MIME message which are accepted by cloud-init: + // - MIME RFC uses CRLF to separate lines - cloud-init is fine with LF \n only + // Note: new lines matters, matters a lot. + var resultArchive = new Array(); + resultArchive.push(`Content-Type: multipart/mixed; boundary="${boundary}"`); + resultArchive.push('MIME-Version: 1.0'); + + // Add new line, the next one will be boundary (encapsulating or closing) + // so this line will count into it. + resultArchive.push(''); + + // Add parts - each part starts with boundary + this.parts.forEach(part => { + resultArchive.push(`--${boundary}`); + resultArchive.push(...part.renderBodyPart()); + }); + + // Add closing boundary + resultArchive.push(`--${boundary}--`); + resultArchive.push(''); // Force new line at the end + + return resultArchive.join('\n'); + } + + public addS3DownloadCommand(_params: S3DownloadOptions): string { + throw new Error(MultipartUserData.USE_PART_ERROR); + } + + public addExecuteFileCommand(_params: ExecuteFileOptions): void { + throw new Error(MultipartUserData.USE_PART_ERROR); + } + + public addSignalOnExitCommand(_resource: Resource): void { + throw new Error(MultipartUserData.USE_PART_ERROR); + } + + public addCommands(..._commands: string[]): void { + throw new Error(MultipartUserData.USE_PART_ERROR); + } + + public addOnExitCommands(..._commands: string[]): void { + throw new Error(MultipartUserData.USE_PART_ERROR); + } +} diff --git a/packages/@aws-cdk/aws-ec2/lib/windows-versions.ts b/packages/@aws-cdk/aws-ec2/lib/windows-versions.ts index b14134eec2363..20e959420168f 100644 --- a/packages/@aws-cdk/aws-ec2/lib/windows-versions.ts +++ b/packages/@aws-cdk/aws-ec2/lib/windows-versions.ts @@ -11,11 +11,15 @@ export enum WindowsVersion { WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_BASE = 'Windows_Server-2012-R2_RTM-Japanese-64Bit-Base', WINDOWS_SERVER_2016_ENGLISH_CORE_CONTAINERS = 'Windows_Server-2016-English-Core-Containers', WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP1_WEB = 'Windows_Server-2016-English-Core-SQL_2016_SP1_Web', + /** @deprecated - use WINDOWS_SERVER_2016_GERMAN_FULL_BASE */ WINDOWS_SERVER_2016_GERMAL_FULL_BASE = 'Windows_Server-2016-German-Full-Base', + WINDOWS_SERVER_2016_GERMAN_FULL_BASE = 'Windows_Server-2016-German-Full-Base', WINDOWS_SERVER_2003_R2_SP2_LANGUAGE_PACKS_32BIT_BASE = 'Windows_Server-2003-R2_SP2-Language_Packs-32Bit-Base', WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2008_R2_SP3_WEB = 'Windows_Server-2008-R2_SP1-English-64Bit-SQL_2008_R2_SP3_Web', WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2012_SP4_EXPRESS = 'Windows_Server-2008-R2_SP1-English-64Bit-SQL_2012_SP4_Express', + /** @deprecated - use WINDOWS_SERVER_2012_R2_SP1_PORTUGUESE_BRAZIL_64BIT_CORE*/ WINDOWS_SERVER_2012_R2_SP1_PORTUGESE_BRAZIL_64BIT_CORE = 'Windows_Server-2008-R2_SP1-Portuguese_Brazil-64Bit-Core', + WINDOWS_SERVER_2012_R2_SP1_PORTUGUESE_BRAZIL_64BIT_CORE = 'Windows_Server-2008-R2_SP1-Portuguese_Brazil-64Bit-Core', WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_SQL_2016_SP2_STANDARD = 'Windows_Server-2012-R2_RTM-English-64Bit-SQL_2016_SP2_Standard', WINDOWS_SERVER_2012_RTM_ENGLISH_64BIT_SQL_2014_SP2_EXPRESS = 'Windows_Server-2012-RTM-English-64Bit-SQL_2014_SP2_Express', WINDOWS_SERVER_2012_RTM_ITALIAN_64BIT_BASE = 'Windows_Server-2012-RTM-Italian-64Bit-Base', @@ -28,7 +32,9 @@ export enum WindowsVersion { WINDOWS_SERVER_2016_JAPANESE_FULL_FQL_2016_SP2_WEB = 'Windows_Server-2016-Japanese-Full-SQL_2016_SP2_Web', WINDOWS_SERVER_2016_KOREAN_FULL_BASE = 'Windows_Server-2016-Korean-Full-Base', WINDOWS_SERVER_2016_KOREAN_FULL_SQL_2016_SP2_STANDARD = 'Windows_Server-2016-Korean-Full-SQL_2016_SP2_Standard', + /** @deprecated - use WINDOWS_SERVER_2016_PORTUGUESE_PORTUGAL_FULL_BASE */ WINDOWS_SERVER_2016_PORTUGESE_PORTUGAL_FULL_BASE = 'Windows_Server-2016-Portuguese_Portugal-Full-Base', + WINDOWS_SERVER_2016_PORTUGUESE_PORTUGAL_FULL_BASE = 'Windows_Server-2016-Portuguese_Portugal-Full-Base', WINDOWS_SERVER_2019_ENGLISH_FULL_SQL_2017_WEB = 'Windows_Server-2019-English-Full-SQL_2017_Web', WINDOWS_SERVER_2019_FRENCH_FULL_BASE = 'Windows_Server-2019-French-Full-Base', WINDOWS_SERVER_2019_KOREAN_FULL_BASE = 'Windows_Server-2019-Korean-Full-Base', @@ -90,8 +96,12 @@ export enum WindowsVersion { WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_SQL_2014_SP2_EXPRESS = 'Windows_Server-2012-R2_RTM-English-64Bit-SQL_2014_SP2_Express', WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_SQL_2014_SP3_WEB = 'Windows_Server-2012-R2_RTM-English-64Bit-SQL_2014_SP3_Web', WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2016_SP1_WEB = 'Windows_Server-2012-R2_RTM-Japanese-64Bit-SQL_2016_SP1_Web', + /** @deprecated - use WINDOWS_SERVER_2012_R2_RTM_PORTUGUESE_BRAZIL_64BIT_BASE */ WINDOWS_SERVER_2012_R2_RTM_PORTUGESE_BRAZIL_64BIT_BASE = 'Windows_Server-2012-R2_RTM-Portuguese_Brazil-64Bit-Base', + WINDOWS_SERVER_2012_R2_RTM_PORTUGUESE_BRAZIL_64BIT_BASE = 'Windows_Server-2012-R2_RTM-Portuguese_Brazil-64Bit-Base', + /** @deprecated - use WINDOWS_SERVER_2012_R2_RTM_PORTUGUESE_PORTUGAL_64BIT_BASE*/ WINDOWS_SERVER_2012_R2_RTM_PORTUGESE_PORTUGAL_64BIT_BASE = 'Windows_Server-2012-R2_RTM-Portuguese_Portugal-64Bit-Base', + WINDOWS_SERVER_2012_R2_RTM_PORTUGUESE_PORTUGAL_64BIT_BASE = 'Windows_Server-2012-R2_RTM-Portuguese_Portugal-64Bit-Base', WINDOWS_SERVER_2012_R2_RTM_SWEDISH_64BIT_BASE = 'Windows_Server-2012-R2_RTM-Swedish-64Bit-Base', WINDOWS_SERVER_2016_ENGLISH_FULL_SQL_2016_SP1_EXPRESS = 'Windows_Server-2016-English-Full-SQL_2016_SP1_Express', WINDOWS_SERVER_2016_ITALIAN_FULL_BASE = 'Windows_Server-2016-Italian-Full-Base', @@ -103,7 +113,9 @@ export enum WindowsVersion { WINDOWS_SERVER_2012_RTM_ENGLISH_64BIT_SQL_2007_R2_SP3_WEB = 'Windows_Server-2012-RTM-English-64Bit-SQL_2008_R2_SP3_Web', WINDOWS_SERVER_2012_RTM_JAPANESE_64BIT_SQL_2014_SP2_WEB = 'Windows_Server-2012-RTM-Japanese-64Bit-SQL_2014_SP2_Web', WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_ENTERPRISE = 'Windows_Server-2016-English-Core-SQL_2016_SP2_Enterprise', + /** @deprecated - use WINDOWS_SERVER_2016_PORTUGUESE_BRAZIL_FULL_BASE */ WINDOWS_SERVER_2016_PORTUGESE_BRAZIL_FULL_BASE = 'Windows_Server-2016-Portuguese_Brazil-Full-Base', + WINDOWS_SERVER_2016_PORTUGUESE_BRAZIL_FULL_BASE = 'Windows_Server-2016-Portuguese_Brazil-Full-Base', WINDOWS_SERVER_2019_ENGLISH_FULL_BASE = 'Windows_Server-2019-English-Full-Base', WINDOWS_SERVER_2003_R2_SP2_ENGLISH_32BIT_BASE = 'Windows_Server-2003-R2_SP2-English-32Bit-Base', WINDOWS_SERVER_2012_R2_RTM_CZECH_64BIT_BASE = 'Windows_Server-2012-R2_RTM-Czech-64Bit-Base', @@ -128,7 +140,9 @@ export enum WindowsVersion { WINDOWS_SERVER_2003_R2_SP2_ENGLISH_64BIT_BASE = 'Windows_Server-2003-R2_SP2-English-64Bit-Base', WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_BASE = 'Windows_Server-2008-R2_SP1-English-64Bit-Base', WINDOWS_SERVER_2008_R2_SP1_LANGUAGE_PACKS_64BIT_SQL_2008_R2_SP3_EXPRESS = 'Windows_Server-2008-R2_SP1-Language_Packs-64Bit-SQL_2008_R2_SP3_Express', + /** @deprecated - use WINDOWS_SERVER_2012_SP2_PORTUGUESE_BRAZIL_64BIT_BASE */ WINDOWS_SERVER_2012_SP2_PORTUGESE_BRAZIL_64BIT_BASE = 'Windows_Server-2008-SP2-Portuguese_Brazil-64Bit-Base', + WINDOWS_SERVER_2012_SP2_PORTUGUESE_BRAZIL_64BIT_BASE = 'Windows_Server-2008-SP2-Portuguese_Brazil-64Bit-Base', WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_SQL_2016_SP1_WEB = 'Windows_Server-2012-R2_RTM-English-64Bit-SQL_2016_SP1_Web', WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2014_SP3_EXPRESS = 'Windows_Server-2012-R2_RTM-Japanese-64Bit-SQL_2014_SP3_Express', WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2016_SP2_ENTERPRISE = 'Windows_Server-2012-R2_RTM-Japanese-64Bit-SQL_2016_SP2_Enterprise', @@ -141,7 +155,9 @@ export enum WindowsVersion { WINDOWS_SERVER_2008_R2_SP1_JAPANESE_64BIT_BASE = 'Windows_Server-2008-R2_SP1-Japanese-64Bit-Base', WINDOWS_SERVER_2008_SP2_ENGLISH_64BIT_SQL_2008_SP4_STANDARD = 'Windows_Server-2008-SP2-English-64Bit-SQL_2008_SP4_Standard', WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_BASE = 'Windows_Server-2012-R2_RTM-English-64Bit-Base', + /** @deprecated - use WINDOWS_SERVER_2012_RTM_PORTUGUESE_BRAZIL_64BIT_BASE */ WINDOWS_SERVER_2012_RTM_PORTUGESE_BRAZIL_64BIT_BASE = 'Windows_Server-2012-RTM-Portuguese_Brazil-64Bit-Base', + WINDOWS_SERVER_2012_RTM_PORTUGUESE_BRAZIL_64BIT_BASE = 'Windows_Server-2012-RTM-Portuguese_Brazil-64Bit-Base', WINDOWS_SERVER_2016_ENGLISH_FULL_SQL_2016_SP1_WEB = 'Windows_Server-2016-English-Full-SQL_2016_SP1_Web', WINDOWS_SERVER_2016_ENGLISH_P3 = 'Windows_Server-2016-English-P3', WINDOWS_SERVER_2016_JAPANESE_FULL_SQL_2016_SP1_ENTERPRISE = 'Windows_Server-2016-Japanese-Full-SQL_2016_SP1_Enterprise', @@ -165,7 +181,9 @@ export enum WindowsVersion { WINDOWS_SERVER_2016_CHINESE_SIMPLIFIED_FULL_BASE = 'Windows_Server-2016-Chinese_Simplified-Full-Base', WINDOWS_SERVER_2019_POLISH_FULL_BASE = 'Windows_Server-2019-Polish-Full-Base', WINDOWS_SERVER_2008_R2_SP1_JAPANESE_64BIT_SQL_2008_R2_SP3_WEB = 'Windows_Server-2008-R2_SP1-Japanese-64Bit-SQL_2008_R2_SP3_Web', + /** @deprecated - use WINDOWS_SERVER_2008_R2_SP1_PORTUGUESE_BRAZIL_64BIT_BASE */ WINDOWS_SERVER_2008_R2_SP1_PORTUGESE_BRAZIL_64BIT_BASE = 'Windows_Server-2008-R2_SP1-Portuguese_Brazil-64Bit-Base', + WINDOWS_SERVER_2008_R2_SP1_PORTUGUESE_BRAZIL_64BIT_BASE = 'Windows_Server-2008-R2_SP1-Portuguese_Brazil-64Bit-Base', WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2016_SP1_ENTERPRISE = 'Windows_Server-2012-R2_RTM-Japanese-64Bit-SQL_2016_SP1_Enterprise', WINDOWS_SERVER_2012_RTM_JAPANESE_64BIT_SQL_2016_SP2_EXPRESS = 'Windows_Server-2012-R2_RTM-Japanese-64Bit-SQL_2016_SP2_Express', WINDOWS_SERVER_2012_RTM_ENGLISH_64BIT_SQL_2014_SP3_EXPRESS = 'Windows_Server-2012-RTM-English-64Bit-SQL_2014_SP3_Express', @@ -196,10 +214,14 @@ export enum WindowsVersion { WINDOWS_SERVER_1709_ENGLISH_CORE_BASE = 'Windows_Server-1709-English-Core-Base', WINDOWS_SERVER_2008_R2_SP1_ENGLISH_61BIT_SQL_2012_RTM_SP2_ENTERPRISE = 'Windows_Server-2008-R2_SP1-English-64Bit-SQL_2012_RTM_SP2_Enterprise', WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2012_SP4_STANDARD = 'Windows_Server-2008-R2_SP1-English-64Bit-SQL_2012_SP4_Standard', + /** @deprecated - use WINDOWS_SERVER_2008_SP2_PORTUGUESE_BRAZIL_32BIT_BASE */ WINDOWS_SERVER_2008_SP2_PORTUGESE_BRAZIL_32BIT_BASE = 'Windows_Server-2008-SP2-Portuguese_Brazil-32Bit-Base', + WINDOWS_SERVER_2008_SP2_PORTUGUESE_BRAZIL_32BIT_BASE = 'Windows_Server-2008-SP2-Portuguese_Brazil-32Bit-Base', WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2014_SP2_STANDARD = 'Windows_Server-2012-R2_RTM-Japanese-64Bit-SQL_2014_SP2_Standard', WINDOWS_SERVER_2012_RTM_JAPANESE_64BIT_SQL_2012_SP4_EXPRESS = 'Windows_Server-2012-RTM-Japanese-64Bit-SQL_2012_SP4_Express', + /** @deprecated - use WINDOWS_SERVER_2012_RTM_PORTUGUESE_PORTUGAL_64BIT_BASE */ WINDOWS_SERVER_2012_RTM_PORTUGESE_PORTUGAL_64BIT_BASE = 'Windows_Server-2012-RTM-Portuguese_Portugal-64Bit-Base', + WINDOWS_SERVER_2012_RTM_PORTUGUESE_PORTUGAL_64BIT_BASE = 'Windows_Server-2012-RTM-Portuguese_Portugal-64Bit-Base', WINDOWS_SERVER_2016_CZECH_FULL_BASE = 'Windows_Server-2016-Czech-Full-Base', WINDOWS_SERVER_2016_JAPANESE_FULL_SQL_2016_SP1_STANDARD = 'Windows_Server-2016-Japanese-Full-SQL_2016_SP1_Standard', WINDOWS_SERVER_2019_DUTCH_FULL_BASE = 'Windows_Server-2019-Dutch-Full-Base', @@ -212,7 +234,9 @@ export enum WindowsVersion { WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_EXPRESS = 'Windows_Server-2016-English-Core-SQL_2016_SP2_Express', WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_WEB = 'Windows_Server-2016-English-Core-SQL_2016_SP2_Web', WINDOWS_SERVER_2016_ENGLISH_FULL_SQL_2017_STANDARD = 'Windows_Server-2016-English-Full-SQL_2017_Standard', + /** @deprecated - use WINDOWS_SERVER_2019_PORTUGUESE_BRAZIL_FULL_BASE */ WINDOWS_SERVER_2019_PORTUGESE_BRAZIL_FULL_BASE = 'Windows_Server-2019-Portuguese_Brazil-Full-Base', + WINDOWS_SERVER_2019_PORTUGUESE_BRAZIL_FULL_BASE = 'Windows_Server-2019-Portuguese_Brazil-Full-Base', WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2008_R2_SP3_STANDARD = 'Windows_Server-2008-R2_SP1-English-64Bit-SQL_2008_R2_SP3_Standard', WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SHAREPOINT_2010_SP2_FOUNDATION = 'Windows_Server-2008-R2_SP1-English-64Bit-SharePoint_2010_SP2_Foundation', WINDOWS_SERVER_2012_R2_RTM_ENGLISH_P3 = 'Windows_Server-2012-R2_RTM-English-P3', @@ -221,7 +245,9 @@ export enum WindowsVersion { WINDOWS_SERVER_2012_RTM_JAPANESE_64BIT_SQL_2014_SP3_EXPRESS = 'Windows_Server-2012-RTM-Japanese-64Bit-SQL_2014_SP3_Express', WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_STANDARD = 'Windows_Server-2016-English-Core-SQL_2016_SP2_Standard', WINDOWS_SERVER_2016_JAPANESE_FULL_SQL_2016_SP2_STANDARD = 'Windows_Server-2016-Japanese-Full-SQL_2016_SP2_Standard', + /** @deprecated - use WINDOWS_SERVER_2019_PORTUGUESE_PORTUGAL_FULL_BASE */ WINDOWS_SERVER_2019_PORTUGESE_PORTUGAL_FULL_BASE = 'Windows_Server-2019-Portuguese_Portugal-Full-Base', + WINDOWS_SERVER_2019_PORTUGUESE_PORTUGAL_FULL_BASE = 'Windows_Server-2019-Portuguese_Portugal-Full-Base', WINDOWS_SERVER_2019_SWEDISH_FULL_BASE = 'Windows_Server-2019-Swedish-Full-Base', WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_HYPERV = 'Windows_Server-2012-R2_RTM-English-64Bit-HyperV', WINDOWS_SERVER_2012_RTM_KOREAN_64BIT_BASE = 'Windows_Server-2012-RTM-Korean-64Bit-Base', diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 801040d4c7385..c5d5d9be6a64e 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -140,6 +140,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_HYPERV", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_SWEDISH_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_PORTUGESE_PORTUGAL_FULL_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_PORTUGUESE_PORTUGAL_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_JAPANESE_FULL_SQL_2016_SP2_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_JAPANESE_64BIT_SQL_2014_SP3_EXPRESS", @@ -149,6 +150,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SHAREPOINT_2010_SP2_FOUNDATION", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2008_R2_SP3_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_PORTUGESE_BRAZIL_FULL_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_PORTUGUESE_BRAZIL_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_FULL_SQL_2017_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_EXPRESS", @@ -162,9 +164,11 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_JAPANESE_FULL_SQL_2016_SP1_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_CZECH_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_PORTUGESE_PORTUGAL_64BIT_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_PORTUGUESE_PORTUGAL_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_JAPANESE_64BIT_SQL_2012_SP4_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2014_SP2_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_SP2_PORTUGESE_BRAZIL_32BIT_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_SP2_PORTUGUESE_BRAZIL_32BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2012_SP4_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_ENGLISH_61BIT_SQL_2012_RTM_SP2_ENTERPRISE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_1709_ENGLISH_CORE_BASE", @@ -315,6 +319,8 @@ "docs-public-apis:@aws-cdk/aws-ec2.Protocol.UDP", "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ICMP", "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ICMPV6", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ESP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.AH", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_SP2_ENGLISH_64BIT_SQL_2008_SP4_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_CHINESE_SIMPLIFIED_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_CHINESE_TRADITIONAL_64BIT_BASE", @@ -325,10 +331,12 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_CORE_CONTAINERS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP1_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_GERMAL_FULL_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_GERMAN_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2003_R2_SP2_LANGUAGE_PACKS_32BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2008_R2_SP3_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_SQL_2012_SP4_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_SP1_PORTUGESE_BRAZIL_64BIT_CORE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_SP1_PORTUGUESE_BRAZIL_64BIT_CORE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_SQL_2016_SP2_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_ENGLISH_64BIT_SQL_2014_SP2_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_ITALIAN_64BIT_BASE", @@ -342,6 +350,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_KOREAN_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_KOREAN_FULL_SQL_2016_SP2_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_PORTUGESE_PORTUGAL_FULL_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_PORTUGUESE_PORTUGAL_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_SQL_2017_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_FRENCH_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_KOREAN_FULL_BASE", @@ -404,7 +413,9 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_SQL_2014_SP3_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2016_SP1_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_PORTUGESE_BRAZIL_64BIT_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_PORTUGUESE_BRAZIL_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_PORTUGESE_PORTUGAL_64BIT_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_PORTUGUESE_PORTUGAL_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_SWEDISH_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_FULL_SQL_2016_SP1_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ITALIAN_FULL_BASE", @@ -417,6 +428,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_JAPANESE_64BIT_SQL_2014_SP2_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_CORE_SQL_2016_SP2_ENTERPRISE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_PORTUGESE_BRAZIL_FULL_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_PORTUGUESE_BRAZIL_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_ENGLISH_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2003_R2_SP2_ENGLISH_32BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_CZECH_64BIT_BASE", @@ -441,6 +453,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_ENGLISH_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_LANGUAGE_PACKS_64BIT_SQL_2008_R2_SP3_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_SP2_PORTUGESE_BRAZIL_64BIT_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_SP2_PORTUGUESE_BRAZIL_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_SQL_2016_SP1_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2014_SP3_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_JAPANESE_64BIT_SQL_2016_SP2_ENTERPRISE", @@ -454,6 +467,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_SP2_ENGLISH_64BIT_SQL_2008_SP4_STANDARD", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_ENGLISH_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_PORTUGESE_BRAZIL_64BIT_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_RTM_PORTUGUESE_BRAZIL_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_FULL_SQL_2016_SP1_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_ENGLISH_P3", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2016_JAPANESE_FULL_SQL_2016_SP1_ENTERPRISE", @@ -478,6 +492,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2019_POLISH_FULL_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_JAPANESE_64BIT_SQL_2008_R2_SP3_WEB", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_PORTUGESE_BRAZIL_64BIT_BASE", + "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_R2_SP1_PORTUGUESE_BRAZIL_64BIT_BASE", "props-default-doc:@aws-cdk/aws-ec2.InterfaceVpcEndpointAttributes.securityGroupId", "props-default-doc:@aws-cdk/aws-ec2.InterfaceVpcEndpointAttributes.securityGroups", "props-physical-name:@aws-cdk/aws-ec2.VpnGatewayProps" diff --git a/packages/@aws-cdk/aws-ec2/test/integ.instance-multipart-userdata.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.instance-multipart-userdata.expected.json new file mode 100644 index 0000000000000..371a30e7456f6 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.instance-multipart-userdata.expected.json @@ -0,0 +1,694 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "TestStackMultipartUserData/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": "TestStackMultipartUserData/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStackMultipartUserData/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": "TestStackMultipartUserData/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStackMultipartUserData/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": "TestStackMultipartUserData/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStackMultipartUserData/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": "TestStackMultipartUserData/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStackMultipartUserData/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": "TestStackMultipartUserData/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTable98AE0E14": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStackMultipartUserData/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": "TestStackMultipartUserData/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3NATGatewayD3048F5C": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet3EIPAD4BC883", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStackMultipartUserData/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": "TestStackMultipartUserData/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStackMultipartUserData/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": "TestStackMultipartUserData/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStackMultipartUserData/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": "TestStackMultipartUserData/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTable192186F8": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStackMultipartUserData/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": "TestStackMultipartUserData/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "InstanceInstanceSecurityGroupF0E2D5BE": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "TestStackMultipartUserData/Instance/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "from 0.0.0.0/0:ICMP Type 8", + "FromPort": 8, + "IpProtocol": "icmp", + "ToPort": -1 + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "TestStackMultipartUserData/Instance" + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "InstanceInstanceRoleE9785DE5": { + "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": "TestStackMultipartUserData/Instance" + } + ] + } + }, + "InstanceInstanceRoleDefaultPolicy4ACE9290": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ssm:*", + "ssmmessages:*", + "ec2messages:GetMessages" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "InstanceInstanceRoleDefaultPolicy4ACE9290", + "Roles": [ + { + "Ref": "InstanceInstanceRoleE9785DE5" + } + ] + } + }, + "InstanceInstanceProfileAB5AEF02": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "InstanceInstanceRoleE9785DE5" + } + ] + } + }, + "InstanceC1063A87": { + "Type": "AWS::EC2::Instance", + "Properties": { + "AvailabilityZone": "test-region-1a", + "IamInstanceProfile": { + "Ref": "InstanceInstanceProfileAB5AEF02" + }, + "ImageId": { + "Ref": "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceType": "t3.nano", + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "InstanceInstanceSecurityGroupF0E2D5BE", + "GroupId" + ] + } + ], + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + "Tags": [ + { + "Key": "Name", + "Value": "TestStackMultipartUserData/Instance" + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "Content-Type: multipart/mixed; boundary=\"+AWS+CDK+User+Data+Separator==\"\nMIME-Version: 1.0\n\n--+AWS+CDK+User+Data+Separator==\nContent-Type: text/x-shellscript; charset=\"utf-8\"\nContent-Transfer-Encoding: base64\n\n", + { + "Fn::Base64": "#!/bin/bash\necho 大らと > /var/tmp/echo1\ncp /var/tmp/echo1 /var/tmp/echo1-copy" + }, + "\n--+AWS+CDK+User+Data+Separator==\nContent-Type: text/x-shellscript; charset=\"utf-8\"\nContent-Transfer-Encoding: base64\n\n", + { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho 大らと ", + { + "Ref": "VPCB9E5F0B4" + }, + " > /var/tmp/echo2" + ] + ] + } + }, + "\n--+AWS+CDK+User+Data+Separator==\nContent-Type: text/cloud-boothook\nContent-Transfer-Encoding: base64\n\n", + { + "Fn::Base64": "#!/bin/bash\necho \"Boothook2\" > /var/tmp/boothook\ncloud-init-per once docker_options echo 'OPTIONS=\"${OPTIONS} --storage-opt dm.basesize=20G\"' >> /etc/sysconfig/docker" + }, + "\n--+AWS+CDK+User+Data+Separator==\nContent-Type: text/x-shellscript\n\necho \"RawPart\" > /var/tmp/rawPart1\n--+AWS+CDK+User+Data+Separator==\nContent-Type: text/x-shellscript\n\necho \"RawPart ", + { + "Ref": "VPCB9E5F0B4" + }, + "\" > /var/tmp/rawPart2\n--+AWS+CDK+User+Data+Separator==\nContent-Type: text/x-shellscript\n\ncp $0 /var/tmp/upstart # Should be one line file no new line at the end and beginning\n--+AWS+CDK+User+Data+Separator==--\n" + ] + ] + } + } + }, + "DependsOn": [ + "InstanceInstanceRoleDefaultPolicy4ACE9290", + "InstanceInstanceRoleE9785DE5" + ] + } + }, + "Parameters": { + "SsmParameterValueawsserviceamiamazonlinuxlatestamzn2amihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/test/integ.instance-multipart-userdata.ts b/packages/@aws-cdk/aws-ec2/test/integ.instance-multipart-userdata.ts new file mode 100644 index 0000000000000..9038166b93e39 --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/integ.instance-multipart-userdata.ts @@ -0,0 +1,70 @@ +/// !cdk-integ * +import { PolicyStatement } from '@aws-cdk/aws-iam'; +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'); + + // Here we test default separator as probably most useful + const multipartUserData = new ec2.MultipartUserData(); + + const userData1 = ec2.UserData.forLinux(); + userData1.addCommands('echo 大らと > /var/tmp/echo1'); + userData1.addCommands('cp /var/tmp/echo1 /var/tmp/echo1-copy'); + + const userData2 = ec2.UserData.forLinux(); + userData2.addCommands(`echo 大らと ${vpc.vpcId} > /var/tmp/echo2`); + + const rawPart1 = ec2.MultipartBody.fromRawBody({ + contentType: 'text/x-shellscript', + body: 'echo "RawPart" > /var/tmp/rawPart1', + }); + + const rawPart2 = ec2.MultipartBody.fromRawBody({ + contentType: 'text/x-shellscript', + body: `echo "RawPart ${vpc.vpcId}" > /var/tmp/rawPart2`, + }); + + const bootHook = ec2.UserData.forLinux(); + bootHook.addCommands( + 'echo "Boothook2" > /var/tmp/boothook', + 'cloud-init-per once docker_options echo \'OPTIONS="${OPTIONS} --storage-opt dm.basesize=20G"\' >> /etc/sysconfig/docker', + ); + + multipartUserData.addPart(ec2.MultipartBody.fromUserData(userData1)); + multipartUserData.addPart(ec2.MultipartBody.fromUserData(userData2)); + multipartUserData.addPart(ec2.MultipartBody.fromUserData(bootHook, 'text/cloud-boothook')); + + const rawPart3 = ec2.MultipartBody.fromRawBody({ + contentType: 'text/x-shellscript', + body: 'cp $0 /var/tmp/upstart # Should be one line file no new line at the end and beginning', + }); + multipartUserData.addPart(rawPart1); + multipartUserData.addPart(rawPart2); + multipartUserData.addPart(rawPart3); + + const instance = new ec2.Instance(this, 'Instance', { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.NANO), + machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2 }), + userData: multipartUserData, + }); + + instance.addToRolePolicy(new PolicyStatement({ + actions: ['ssm:*', 'ssmmessages:*', 'ec2messages:GetMessages'], + resources: ['*'], + })); + + instance.connections.allowFromAnyIpv4(ec2.Port.icmpPing()); + } +} + +new TestStack(app, 'TestStackMultipartUserData'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json index 1586cc8bff5e7..641b97b4ddbd5 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json @@ -567,6 +567,20 @@ "FromPort": 800, "IpProtocol": "udp", "ToPort": 801 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "from 0.0.0.0/0:ESP 50", + "FromPort": 50, + "IpProtocol": "esp", + "ToPort": 50 + }, + { + "CidrIp": "0.0.0.0/0", + "Description": "from 0.0.0.0/0:AH 51", + "FromPort": 51, + "IpProtocol": "ah", + "ToPort": 51 } ], "VpcId": { @@ -575,4 +589,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts b/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts index 2ffd5653e33f4..88e4dacf9839a 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts @@ -16,6 +16,8 @@ const rules = [ ec2.Port.allUdp(), ec2.Port.udp(123), ec2.Port.udpRange(800, 801), + ec2.Port.esp(), + ec2.Port.ah(), ]; for (const rule of rules) { diff --git a/packages/@aws-cdk/aws-ec2/test/userdata.test.ts b/packages/@aws-cdk/aws-ec2/test/userdata.test.ts index 883794bd5c585..26493962cbbb8 100644 --- a/packages/@aws-cdk/aws-ec2/test/userdata.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/userdata.test.ts @@ -273,4 +273,101 @@ nodeunitShim({ test.done(); }, + 'Linux user rendering multipart headers'(test: Test) { + // GIVEN + const stack = new Stack(); + const linuxUserData = ec2.UserData.forLinux(); + linuxUserData.addCommands('echo "Hello world"'); + + // WHEN + const defaultRender1 = ec2.MultipartBody.fromUserData(linuxUserData); + const defaultRender2 = ec2.MultipartBody.fromUserData(linuxUserData, 'text/cloud-boothook; charset=\"utf-8\"'); + + // THEN + expect(stack.resolve(defaultRender1.renderBodyPart())).toEqual([ + 'Content-Type: text/x-shellscript; charset=\"utf-8\"', + 'Content-Transfer-Encoding: base64', + '', + { 'Fn::Base64': '#!/bin/bash\necho \"Hello world\"' }, + ]); + expect(stack.resolve(defaultRender2.renderBodyPart())).toEqual([ + 'Content-Type: text/cloud-boothook; charset=\"utf-8\"', + 'Content-Transfer-Encoding: base64', + '', + { 'Fn::Base64': '#!/bin/bash\necho \"Hello world\"' }, + ]); + + test.done(); + }, + + 'Default parts separator used, if not specified'(test: Test) { + // GIVEN + const multipart = new ec2.MultipartUserData(); + + multipart.addPart(ec2.MultipartBody.fromRawBody({ + contentType: 'CT', + })); + + // WHEN + const out = multipart.render(); + + // WHEN + test.equals(out, [ + 'Content-Type: multipart/mixed; boundary=\"+AWS+CDK+User+Data+Separator==\"', + 'MIME-Version: 1.0', + '', + '--+AWS+CDK+User+Data+Separator==', + 'Content-Type: CT', + '', + '--+AWS+CDK+User+Data+Separator==--', + '', + ].join('\n')); + + test.done(); + }, + + 'Non-default parts separator used, if not specified'(test: Test) { + // GIVEN + const multipart = new ec2.MultipartUserData({ + partsSeparator: '//', + }); + + multipart.addPart(ec2.MultipartBody.fromRawBody({ + contentType: 'CT', + })); + + // WHEN + const out = multipart.render(); + + // WHEN + test.equals(out, [ + 'Content-Type: multipart/mixed; boundary=\"//\"', + 'MIME-Version: 1.0', + '', + '--//', + 'Content-Type: CT', + '', + '--//--', + '', + ].join('\n')); + + test.done(); + }, + + 'Multipart separator validation'(test: Test) { + // Happy path + new ec2.MultipartUserData(); + new ec2.MultipartUserData({ + partsSeparator: 'a-zA-Z0-9()+,-./:=?', + }); + + [' ', '\n', '\r', '[', ']', '<', '>', '違う'].forEach(s => test.throws(() => { + new ec2.MultipartUserData({ + partsSeparator: s, + }); + }, /Invalid characters in separator/)); + + test.done(); + }, + }); diff --git a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts index 9f4ceaf1357d6..77de63770cd6a 100644 --- a/packages/@aws-cdk/aws-ec2/test/vpc.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/vpc.test.ts @@ -2,9 +2,30 @@ import { countResources, expect as cdkExpect, haveResource, haveResourceLike, is import { CfnOutput, CfnResource, Fn, Lazy, Stack, Tags } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; import { - AclCidr, AclTraffic, BastionHostLinux, CfnSubnet, CfnVPC, SubnetFilter, DefaultInstanceTenancy, GenericLinuxImage, - InstanceType, InterfaceVpcEndpoint, InterfaceVpcEndpointService, NatProvider, NetworkAcl, NetworkAclEntry, Peer, Port, PrivateSubnet, - PublicSubnet, RouterType, Subnet, SubnetType, TrafficDirection, Vpc, + AclCidr, + AclTraffic, + BastionHostLinux, + CfnSubnet, + CfnVPC, + SubnetFilter, + DefaultInstanceTenancy, + GenericLinuxImage, + InstanceType, + InterfaceVpcEndpoint, + InterfaceVpcEndpointService, + NatProvider, + NatTrafficDirection, + NetworkAcl, + NetworkAclEntry, + Peer, + Port, + PrivateSubnet, + PublicSubnet, + RouterType, + Subnet, + SubnetType, + TrafficDirection, + Vpc, } from '../lib'; nodeunitShim({ @@ -904,6 +925,22 @@ nodeunitShim({ DestinationCidrBlock: '0.0.0.0/0', InstanceId: { Ref: 'TheVPCPublicSubnet1NatInstanceCC514192' }, })); + cdkExpect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + SecurityGroupIngress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'from 0.0.0.0/0:ALL TRAFFIC', + IpProtocol: '-1', + }, + ], + })); test.done(); }, @@ -929,7 +966,7 @@ nodeunitShim({ test.done(); }, - 'can configure Security Groups of NAT instances'(test: Test) { + 'can configure Security Groups of NAT instances with allowAllTraffic false'(test: Test) { // GIVEN const stack = getTestStack(); @@ -948,6 +985,13 @@ nodeunitShim({ // THEN cdkExpect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], SecurityGroupIngress: [ { CidrIp: '1.2.3.4/32', @@ -962,6 +1006,105 @@ nodeunitShim({ test.done(); }, + 'can configure Security Groups of NAT instances with defaultAllowAll INBOUND_AND_OUTBOUND'(test: Test) { + // GIVEN + const stack = getTestStack(); + + // WHEN + const provider = NatProvider.instance({ + instanceType: new InstanceType('q86.mega'), + machineImage: new GenericLinuxImage({ + 'us-east-1': 'ami-1', + }), + defaultAllowedTraffic: NatTrafficDirection.INBOUND_AND_OUTBOUND, + }); + new Vpc(stack, 'TheVPC', { + natGatewayProvider: provider, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + SecurityGroupIngress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'from 0.0.0.0/0:ALL TRAFFIC', + IpProtocol: '-1', + }, + ], + })); + + test.done(); + }, + + 'can configure Security Groups of NAT instances with defaultAllowAll OUTBOUND_ONLY'(test: Test) { + // GIVEN + const stack = getTestStack(); + + // WHEN + const provider = NatProvider.instance({ + instanceType: new InstanceType('q86.mega'), + machineImage: new GenericLinuxImage({ + 'us-east-1': 'ami-1', + }), + defaultAllowedTraffic: NatTrafficDirection.OUTBOUND_ONLY, + }); + new Vpc(stack, 'TheVPC', { + natGatewayProvider: provider, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + })); + + test.done(); + }, + + 'can configure Security Groups of NAT instances with defaultAllowAll NONE'(test: Test) { + // GIVEN + const stack = getTestStack(); + + // WHEN + const provider = NatProvider.instance({ + instanceType: new InstanceType('q86.mega'), + machineImage: new GenericLinuxImage({ + 'us-east-1': 'ami-1', + }), + defaultAllowedTraffic: NatTrafficDirection.NONE, + }); + new Vpc(stack, 'TheVPC', { + natGatewayProvider: provider, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + SecurityGroupEgress: [ + { + CidrIp: '255.255.255.255/32', + Description: 'Disallow all traffic', + FromPort: 252, + IpProtocol: 'icmp', + ToPort: 86, + }, + ], + })); + + test.done(); + }, + }, 'Network ACL association': { diff --git a/packages/@aws-cdk/aws-ecr/README.md b/packages/@aws-cdk/aws-ecr/README.md index 9bce0c80e132c..6adb791ba7f76 100644 --- a/packages/@aws-cdk/aws-ecr/README.md +++ b/packages/@aws-cdk/aws-ecr/README.md @@ -57,7 +57,7 @@ const user = new iam.User(this, '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 +If you access images in the [Public ECR Gallery](https://gallery.ecr.aws/) as well, it is recommended you authenticate to the registry 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). diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 3787903dedc3e..20f110c206428 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -450,7 +450,7 @@ export class Repository extends RepositoryBase { repositoryPolicyText: Lazy.any({ produce: () => this.policyDocument }), lifecyclePolicy: Lazy.any({ produce: () => this.renderLifecyclePolicy() }), imageScanningConfiguration: !props.imageScanOnPush ? undefined : { - scanOnPush: true, + ScanOnPush: true, }, }); diff --git a/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json b/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json index 5367d722f62c9..5fbca07aa35d4 100644 --- a/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json +++ b/packages/@aws-cdk/aws-ecr/test/integ.imagescan.expected.json @@ -4,7 +4,7 @@ "Type": "AWS::ECR::Repository", "Properties": { "ImageScanningConfiguration": { - "scanOnPush": true + "ScanOnPush": true } }, "UpdateReplacePolicy": "Delete", @@ -87,4 +87,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecr/test/test.repository.ts b/packages/@aws-cdk/aws-ecr/test/test.repository.ts index 20c53f1a3032a..8c8094be287e9 100644 --- a/packages/@aws-cdk/aws-ecr/test/test.repository.ts +++ b/packages/@aws-cdk/aws-ecr/test/test.repository.ts @@ -38,7 +38,7 @@ export = { // THEN expect(stack).to(haveResource('AWS::ECR::Repository', { ImageScanningConfiguration: { - scanOnPush: true, + ScanOnPush: true, }, })); test.done(); diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 09a5ad13c14cd..8ac684943e244 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -299,6 +299,8 @@ obtained from either DockerHub or from ECR repositories, or built directly from image directly from a `Dockerfile` in your source directory. - `ecs.ContainerImage.fromDockerImageAsset(asset)`: uses an existing `@aws-cdk/aws-ecr-assets.DockerImageAsset` as a container image. +- `new ecs.TagParameterContainerImage(repository)`: use the given ECR repository as the image + but a CloudFormation parameter as the tag. ### Environment variables @@ -683,6 +685,49 @@ taskDefinition.addContainer('TheContainer', { }); ``` +## CloudMap Service Discovery + +To register your ECS service with a CloudMap Service Registry, you may add the +`cloudMapOptions` property to your service: + +```ts +const service = new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + // Create A records - useful for AWSVPC network mode. + dnsRecordType: cloudmap.DnsRecordType.A, + }, +}); +``` + +With `bridge` or `host` network modes, only `SRV` DNS record types are supported. +By default, `SRV` DNS record types will target the default container and default +port. However, you may target a different container and port on the same ECS task: + +```ts +// Add a container to the task definition +const specificContainer = taskDefinition.addContainer(...); + +// Add a port mapping +specificContainer.addPortMappings({ + containerPort: 7600, + protocol: ecs.Protocol.TCP, +}); + +new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + // Create SRV records - useful for bridge networking + dnsRecordType: cloudmap.DnsRecordType.SRV, + // Targets port TCP port 7600 `specificContainer` + container: specificContainer, + containerPort: 7600, + }, +}); +``` + ## Capacity Providers Currently, only `FARGATE` and `FARGATE_SPOT` capacity providers are supported. 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 8e2483f98838f..1e1fdde585f1e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -8,8 +8,8 @@ import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import { Annotations, Duration, IResolvable, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { LoadBalancerTargetOptions, NetworkMode, TaskDefinition } from '../base/task-definition'; +import { ContainerDefinition, Protocol } from '../container-definition'; import { ICluster, CapacityProviderStrategy } from '../cluster'; -import { Protocol } from '../container-definition'; import { CfnService } from '../ecs.generated'; import { ScalableTaskCount } from './scalable-task-count'; @@ -572,10 +572,12 @@ export abstract class BaseService extends Resource } } - // If the task definition that your service task specifies uses the AWSVPC network mode and a type SRV DNS record is - // used, you must specify a containerName and containerPort combination - const containerName = dnsRecordType === cloudmap.DnsRecordType.SRV ? this.taskDefinition.defaultContainer!.containerName : undefined; - const containerPort = dnsRecordType === cloudmap.DnsRecordType.SRV ? this.taskDefinition.defaultContainer!.containerPort : undefined; + const { containerName, containerPort } = determineContainerNameAndPort({ + taskDefinition: this.taskDefinition, + dnsRecordType: dnsRecordType!, + container: options.container, + containerPort: options.containerPort, + }); const cloudmapService = new cloudmap.Service(this, 'CloudmapService', { namespace: sdNamespace, @@ -799,7 +801,19 @@ export interface CloudMapOptions { * * NOTE: This is used for HealthCheckCustomConfig */ - readonly failureThreshold?: number, + readonly failureThreshold?: number; + + /** + * The container to point to for a SRV record. + * @default - the task definition's default container + */ + readonly container?: ContainerDefinition; + + /** + * The port to point to for a SRV record. + * @default - the default port of the task definition's default container + */ + readonly containerPort?: number; } /** @@ -885,3 +899,42 @@ export enum PropagatedTagSource { */ NONE = 'NONE' } + +/** + * Options for `determineContainerNameAndPort` + */ +interface DetermineContainerNameAndPortOptions { + dnsRecordType: cloudmap.DnsRecordType; + taskDefinition: TaskDefinition; + container?: ContainerDefinition; + containerPort?: number; +} + +/** + * Determine the name of the container and port to target for the service registry. + */ +function determineContainerNameAndPort(options: DetermineContainerNameAndPortOptions) { + // If the record type is SRV, then provide the containerName and containerPort to target. + // We use the name of the default container and the default port of the default container + // unless the user specifies otherwise. + if (options.dnsRecordType === cloudmap.DnsRecordType.SRV) { + // Ensure the user-provided container is from the right task definition. + if (options.container && options.container.taskDefinition != options.taskDefinition) { + throw new Error('Cannot add discovery for a container from another task definition'); + } + + const container = options.container ?? options.taskDefinition.defaultContainer!; + + // Ensure that any port given by the user is mapped. + if (options.containerPort && !container.portMappings.some(mapping => mapping.containerPort === options.containerPort)) { + throw new Error('Cannot add discovery for a container port that has not been mapped'); + } + + return { + containerName: container.containerName, + containerPort: options.containerPort ?? options.taskDefinition.defaultContainer!.containerPort, + }; + } + + return {}; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/lib/images/tag-parameter-container-image.ts b/packages/@aws-cdk/aws-ecs/lib/images/tag-parameter-container-image.ts index b033e8783a514..c6479f44b951b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/images/tag-parameter-container-image.ts +++ b/packages/@aws-cdk/aws-ecs/lib/images/tag-parameter-container-image.ts @@ -49,4 +49,20 @@ export class TagParameterContainerImage extends ContainerImage { }, }); } + + /** + * Returns the value of the CloudFormation Parameter that represents the tag of the image + * in the ECR repository. + */ + public get tagParameterValue(): string { + return cdk.Lazy.string({ + produce: () => { + if (this.imageTagParameter) { + return this.imageTagParameter.valueAsString; + } else { + throw new Error('TagParameterContainerImage must be used in a container definition when using tagParameterValue'); + } + }, + }); + } } diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts index d88c5ecafd1d7..945832a09b869 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts @@ -2190,6 +2190,268 @@ nodeunitShim({ test.done(); }, + + 'user can select any container and port'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'FargateTaskDef', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + + const mainContainer = taskDefinition.addContainer('MainContainer', { + image: ecs.ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512, + }); + mainContainer.addPortMappings({ containerPort: 8000 }); + + const otherContainer = taskDefinition.addContainer('OtherContainer', { + image: ecs.ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512, + }); + otherContainer.addPortMappings({ containerPort: 8001 }); + + // WHEN + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + dnsRecordType: cloudmap.DnsRecordType.SRV, + container: otherContainer, + containerPort: 8001, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + ServiceRegistries: [ + { + RegistryArn: { 'Fn::GetAtt': ['ServiceCloudmapService046058A4', 'Arn'] }, + ContainerName: 'OtherContainer', + ContainerPort: 8001, + }, + ], + })); + + test.done(); + }, + + 'By default, the container name is the default'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Task', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + + taskDefinition.addContainer('main', { + image: ecs.ContainerImage.fromRegistry('some'), + memoryLimitMiB: 512, + }).addPortMappings({ containerPort: 1234 }); + + taskDefinition.addContainer('second', { + image: ecs.ContainerImage.fromRegistry('some'), + memoryLimitMiB: 512, + }).addPortMappings({ containerPort: 4321 }); + + // WHEN + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: {}, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + ServiceRegistries: [{ + ContainerName: 'main', + ContainerPort: undefined, + }], + })); + + test.done(); + }, + + 'For SRV, by default, container name is default container and port is the default container port'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Task', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + + taskDefinition.addContainer('main', { + image: ecs.ContainerImage.fromRegistry('some'), + memoryLimitMiB: 512, + }).addPortMappings({ containerPort: 1234 }); + + taskDefinition.addContainer('second', { + image: ecs.ContainerImage.fromRegistry('some'), + memoryLimitMiB: 512, + }).addPortMappings({ containerPort: 4321 }); + + // WHEN + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + dnsRecordType: cloudmap.DnsRecordType.SRV, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + ServiceRegistries: [{ + ContainerName: 'main', + ContainerPort: 1234, + }], + })); + + test.done(); + }, + + 'allows SRV service discovery to select the container and port'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Task', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + + taskDefinition.addContainer('main', { + image: ecs.ContainerImage.fromRegistry('some'), + memoryLimitMiB: 512, + }).addPortMappings({ containerPort: 1234 }); + + const secondContainer = taskDefinition.addContainer('second', { + image: ecs.ContainerImage.fromRegistry('some'), + memoryLimitMiB: 512, + }); + secondContainer.addPortMappings({ containerPort: 4321 }); + + // WHEN + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + dnsRecordType: cloudmap.DnsRecordType.SRV, + container: secondContainer, + containerPort: 4321, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + ServiceRegistries: [{ + ContainerName: 'second', + ContainerPort: 4321, + }], + })); + + test.done(); + }, + + 'throws if SRV and container is not part of task definition'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Task', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + + // The right container + taskDefinition.addContainer('MainContainer', { + image: ecs.ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512, + }); + + const wrongTaskDefinition = new ecs.Ec2TaskDefinition(stack, 'WrongTaskDef'); + // The wrong container + const wrongContainer = wrongTaskDefinition.addContainer('MainContainer', { + image: ecs.ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512, + }); + + // WHEN + test.throws(() => { + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + dnsRecordType: cloudmap.DnsRecordType.SRV, + container: wrongContainer, + containerPort: 4321, + }, + }); + }, /another task definition/i); + + test.done(); + }, + + 'throws if SRV and the container port is not mapped'(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Task', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + + const container = taskDefinition.addContainer('MainContainer', { + image: ecs.ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512, + }); + + container.addPortMappings({ containerPort: 8000 }); + + test.throws(() => { + new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + dnsRecordType: cloudmap.DnsRecordType.SRV, + container: container, + containerPort: 4321, + }, + }); + }, /container port.*not.*mapped/i); + + test.done(); + }, }, 'Metric'(test: Test) { diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.cloudmap-container-port.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.cloudmap-container-port.expected.json new file mode 100644 index 0000000000000..e067e7d75a67d --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.cloudmap-container-port.expected.json @@ -0,0 +1,808 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcpubSubnet1Subnet410C08CF": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/24", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "pub" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/pubSubnet1" + } + ] + } + }, + "VpcpubSubnet1RouteTableE0483FDA": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/pubSubnet1" + } + ] + } + }, + "VpcpubSubnet1RouteTableAssociation68036D8C": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcpubSubnet1RouteTableE0483FDA" + }, + "SubnetId": { + "Ref": "VpcpubSubnet1Subnet410C08CF" + } + } + }, + "VpcpubSubnet1DefaultRouteF020A9EF": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcpubSubnet1RouteTableE0483FDA" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcpubSubnet2Subnet44A37A0D": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.1.0/24", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "pub" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/pubSubnet2" + } + ] + } + }, + "VpcpubSubnet2RouteTable5A29DF40": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/pubSubnet2" + } + ] + } + }, + "VpcpubSubnet2RouteTableAssociationFB826925": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcpubSubnet2RouteTable5A29DF40" + }, + "SubnetId": { + "Ref": "VpcpubSubnet2Subnet44A37A0D" + } + } + }, + "VpcpubSubnet2DefaultRouteE6D48BA4": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcpubSubnet2RouteTable5A29DF40" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "FargateCluster7CCD5F93": { + "Type": "AWS::ECS::Cluster" + }, + "FargateClustercapacityInstanceSecurityGroupCB3AEDA1": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ/FargateCluster/capacity/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "from 0.0.0.0/0:32768-61000", + "FromPort": 32768, + "IpProtocol": "tcp", + "ToPort": 61000 + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/FargateCluster/capacity" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "FargateClustercapacityInstanceRoleBE253D2D": { + "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": "aws-ecs-integ/FargateCluster/capacity" + } + ] + } + }, + "FargateClustercapacityInstanceRoleDefaultPolicy90B38927": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecs:DeregisterContainerInstance", + "ecs:RegisterContainerInstance", + "ecs:Submit*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:Poll", + "ecs:StartTelemetrySession" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:DiscoverPollEndpoint", + "ecr:GetAuthorizationToken", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "FargateClustercapacityInstanceRoleDefaultPolicy90B38927", + "Roles": [ + { + "Ref": "FargateClustercapacityInstanceRoleBE253D2D" + } + ] + } + }, + "FargateClustercapacityInstanceProfile8294296C": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "FargateClustercapacityInstanceRoleBE253D2D" + } + ] + } + }, + "FargateClustercapacityLaunchConfig9B95CCB7": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": { + "Ref": "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceType": "t3.micro", + "IamInstanceProfile": { + "Ref": "FargateClustercapacityInstanceProfile8294296C" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "FargateClustercapacityInstanceSecurityGroupCB3AEDA1", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\necho ECS_CLUSTER=", + { + "Ref": "FargateCluster7CCD5F93" + }, + " >> /etc/ecs/ecs.config\nsudo iptables --insert FORWARD 1 --in-interface docker+ --destination 169.254.169.254/32 --jump DROP\nsudo service iptables save\necho ECS_AWSVPC_BLOCK_IMDS=true >> /etc/ecs/ecs.config" + ] + ] + } + } + }, + "DependsOn": [ + "FargateClustercapacityInstanceRoleDefaultPolicy90B38927", + "FargateClustercapacityInstanceRoleBE253D2D" + ] + }, + "FargateClustercapacityASGE4034F96": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "1", + "DesiredCapacity": "1", + "LaunchConfigurationName": { + "Ref": "FargateClustercapacityLaunchConfig9B95CCB7" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-ecs-integ/FargateCluster/capacity" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VpcpubSubnet1Subnet410C08CF" + }, + { + "Ref": "VpcpubSubnet2Subnet44A37A0D" + } + ] + }, + "UpdatePolicy": { + "AutoScalingReplacingUpdate": { + "WillReplace": true + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "FargateClustercapacityDrainECSHookFunctionServiceRoleA28505D9": { + "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" + ] + ] + } + ], + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/FargateCluster/capacity" + } + ] + } + }, + "FargateClustercapacityDrainECSHookFunctionServiceRoleDefaultPolicy53CD1145": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:DescribeInstances", + "ec2:DescribeInstanceAttribute", + "ec2:DescribeInstanceStatus", + "ec2:DescribeHosts" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "autoscaling:CompleteLifecycleAction", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":autoscaling:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":autoScalingGroup:*:autoScalingGroupName/", + { + "Ref": "FargateClustercapacityASGE4034F96" + } + ] + ] + } + }, + { + "Action": [ + "ecs:DescribeContainerInstances", + "ecs:DescribeTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "ecs:ListContainerInstances", + "ecs:SubmitContainerStateChange", + "ecs:SubmitTaskStateChange" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + }, + { + "Action": [ + "ecs:UpdateContainerInstancesState", + "ecs:ListTasks" + ], + "Condition": { + "ArnEquals": { + "ecs:cluster": { + "Fn::GetAtt": [ + "FargateCluster7CCD5F93", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "FargateClustercapacityDrainECSHookFunctionServiceRoleDefaultPolicy53CD1145", + "Roles": [ + { + "Ref": "FargateClustercapacityDrainECSHookFunctionServiceRoleA28505D9" + } + ] + } + }, + "FargateClustercapacityDrainECSHookFunction3E60E6D0": { + "Type": "AWS::Lambda::Function", + "Properties": { + "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" + }, + "Role": { + "Fn::GetAtt": [ + "FargateClustercapacityDrainECSHookFunctionServiceRoleA28505D9", + "Arn" + ] + }, + "Environment": { + "Variables": { + "CLUSTER": { + "Ref": "FargateCluster7CCD5F93" + } + } + }, + "Handler": "index.lambda_handler", + "Runtime": "python3.6", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/FargateCluster/capacity" + } + ], + "Timeout": 310 + }, + "DependsOn": [ + "FargateClustercapacityDrainECSHookFunctionServiceRoleDefaultPolicy53CD1145", + "FargateClustercapacityDrainECSHookFunctionServiceRoleA28505D9" + ] + }, + "FargateClustercapacityDrainECSHookFunctionAllowInvokeawsecsintegFargateClustercapacityLifecycleHookDrainHookTopic07C1229F3B6FF246": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "FargateClustercapacityDrainECSHookFunction3E60E6D0", + "Arn" + ] + }, + "Principal": "sns.amazonaws.com", + "SourceArn": { + "Ref": "FargateClustercapacityLifecycleHookDrainHookTopic390A0E34" + } + } + }, + "FargateClustercapacityDrainECSHookFunctionTopic7D6C4884": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "lambda", + "TopicArn": { + "Ref": "FargateClustercapacityLifecycleHookDrainHookTopic390A0E34" + }, + "Endpoint": { + "Fn::GetAtt": [ + "FargateClustercapacityDrainECSHookFunction3E60E6D0", + "Arn" + ] + } + } + }, + "FargateClustercapacityLifecycleHookDrainHookRoleDD26E39B": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "autoscaling.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/FargateCluster/capacity" + } + ] + } + }, + "FargateClustercapacityLifecycleHookDrainHookRoleDefaultPolicyACCDDB70": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "FargateClustercapacityLifecycleHookDrainHookTopic390A0E34" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "FargateClustercapacityLifecycleHookDrainHookRoleDefaultPolicyACCDDB70", + "Roles": [ + { + "Ref": "FargateClustercapacityLifecycleHookDrainHookRoleDD26E39B" + } + ] + } + }, + "FargateClustercapacityLifecycleHookDrainHookTopic390A0E34": { + "Type": "AWS::SNS::Topic", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/FargateCluster/capacity" + } + ] + } + }, + "FargateClustercapacityLifecycleHookDrainHook8AEDE53B": { + "Type": "AWS::AutoScaling::LifecycleHook", + "Properties": { + "AutoScalingGroupName": { + "Ref": "FargateClustercapacityASGE4034F96" + }, + "LifecycleTransition": "autoscaling:EC2_INSTANCE_TERMINATING", + "DefaultResult": "CONTINUE", + "HeartbeatTimeout": 300, + "NotificationTargetARN": { + "Ref": "FargateClustercapacityLifecycleHookDrainHookTopic390A0E34" + }, + "RoleARN": { + "Fn::GetAtt": [ + "FargateClustercapacityLifecycleHookDrainHookRoleDD26E39B", + "Arn" + ] + } + }, + "DependsOn": [ + "FargateClustercapacityLifecycleHookDrainHookRoleDefaultPolicyACCDDB70", + "FargateClustercapacityLifecycleHookDrainHookRoleDD26E39B" + ] + }, + "FargateClusterDefaultServiceDiscoveryNamespace04381E1E": { + "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace", + "Properties": { + "Name": "aws-ecs-integ", + "Vpc": { + "Ref": "Vpc8378EB38" + } + } + }, + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "nginx", + "Memory": 512, + "MemoryReservation": 32, + "Name": "nginx", + "PortMappings": [ + { + "ContainerPort": 80, + "HostPort": 0, + "Protocol": "tcp" + } + ] + }, + { + "Environment": [ + { + "Name": "PORT", + "Value": "81" + } + ], + "Essential": true, + "Image": "nathanpeck/name", + "Memory": 512, + "MemoryReservation": 32, + "Name": "name", + "PortMappings": [ + { + "ContainerPort": 81, + "HostPort": 0, + "Protocol": "tcp" + } + ] + } + ], + "Family": "awsecsintegTaskDef6FDFB69A", + "NetworkMode": "bridge", + "RequiresCompatibilities": [ + "EC2" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + } + } + }, + "ServiceD69D759B": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "FargateCluster7CCD5F93" + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DesiredCount": 3, + "EnableECSManagedTags": false, + "LaunchType": "EC2", + "SchedulingStrategy": "REPLICA", + "ServiceRegistries": [ + { + "ContainerName": "name", + "ContainerPort": 81, + "RegistryArn": { + "Fn::GetAtt": [ + "ServiceCloudmapService046058A4", + "Arn" + ] + } + } + ], + "TaskDefinition": { + "Ref": "TaskDef54694570" + } + } + }, + "ServiceCloudmapService046058A4": { + "Type": "AWS::ServiceDiscovery::Service", + "Properties": { + "DnsConfig": { + "DnsRecords": [ + { + "TTL": 60, + "Type": "SRV" + } + ], + "NamespaceId": { + "Fn::GetAtt": [ + "FargateClusterDefaultServiceDiscoveryNamespace04381E1E", + "Id" + ] + }, + "RoutingPolicy": "MULTIVALUE" + }, + "HealthCheckCustomConfig": { + "FailureThreshold": 1 + }, + "NamespaceId": { + "Fn::GetAtt": [ + "FargateClusterDefaultServiceDiscoveryNamespace04381E1E", + "Id" + ] + } + } + } + }, + "Parameters": { + "SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.cloudmap-container-port.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.cloudmap-container-port.ts new file mode 100644 index 0000000000000..6c1fadbb23666 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.cloudmap-container-port.ts @@ -0,0 +1,70 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cloudmap from '@aws-cdk/aws-servicediscovery'; +import * as cdk from '@aws-cdk/core'; +import * as ecs from '../../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecs-integ'); +const vpc = new ec2.Vpc(stack, 'Vpc', { + maxAzs: 2, + subnetConfiguration: [ + { + name: 'pub', + cidrMask: 24, + subnetType: ec2.SubnetType.PUBLIC, + }, + ], +}); +const cluster = new ecs.Cluster(stack, 'FargateCluster', { vpc }); + +const capacity = cluster.addCapacity('capacity', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO), + desiredCapacity: 1, + minCapacity: 1, + maxCapacity: 1, +}); +capacity.connections.allowFromAnyIpv4(ec2.Port.tcpRange(32768, 61000)); + +cluster.addDefaultCloudMapNamespace({ name: 'aws-ecs-integ' }); + +const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef', {}); + +// Main container +const mainContainer = taskDefinition.addContainer('nginx', { + image: ecs.ContainerImage.fromRegistry('nginx'), + memoryReservationMiB: 32, + memoryLimitMiB: 512, +}); + +mainContainer.addPortMappings({ + containerPort: 80, + protocol: ecs.Protocol.TCP, +}); + +// Name container with SRV +const nameContainer = taskDefinition.addContainer('name', { + image: ecs.ContainerImage.fromRegistry('nathanpeck/name'), + environment: { + PORT: '81', + }, + memoryReservationMiB: 32, + memoryLimitMiB: 512, +}); + +nameContainer.addPortMappings({ + containerPort: 81, + protocol: ecs.Protocol.TCP, +}); + +new ecs.Ec2Service(stack, 'Service', { + cluster, + taskDefinition, + desiredCount: 3, + cloudMapOptions: { + container: nameContainer, + containerPort: 81, + dnsRecordType: cloudmap.DnsRecordType.SRV, + }, +}); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts index c7aa1fc633a1d..c982920225214 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts @@ -1834,6 +1834,52 @@ nodeunitShim({ test.done(); }, + + 'user can select any container and port'(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + cluster.addDefaultCloudMapNamespace({ + name: 'foo.com', + type: cloudmap.NamespaceType.DNS_PRIVATE, + }); + + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const mainContainer = taskDefinition.addContainer('MainContainer', { + image: ecs.ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512, + }); + mainContainer.addPortMappings({ containerPort: 8000 }); + + const otherContainer = taskDefinition.addContainer('OtherContainer', { + image: ecs.ContainerImage.fromRegistry('hello'), + memoryLimitMiB: 512, + }); + otherContainer.addPortMappings({ containerPort: 8001 }); + + new ecs.FargateService(stack, 'Service', { + cluster, + taskDefinition, + cloudMapOptions: { + dnsRecordType: cloudmap.DnsRecordType.SRV, + container: otherContainer, + containerPort: 8001, + }, + }); + + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + ServiceRegistries: [ + { + RegistryArn: { 'Fn::GetAtt': ['ServiceCloudmapService046058A4', 'Arn'] }, + ContainerName: 'OtherContainer', + ContainerPort: 8001, + }, + ], + })); + + test.done(); + }, }, 'Metric'(test: Test) { diff --git a/packages/@aws-cdk/aws-ecs/test/images/tag-parameter-container-image.test.ts b/packages/@aws-cdk/aws-ecs/test/images/tag-parameter-container-image.test.ts index d0fe101252e26..d74ecb5335ac3 100644 --- a/packages/@aws-cdk/aws-ecs/test/images/tag-parameter-container-image.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/images/tag-parameter-container-image.test.ts @@ -21,5 +21,21 @@ nodeunitShim({ test.done(); }, + + 'throws an error when tagParameterValue() is used without binding the image'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const repository = new ecr.Repository(stack, 'Repository'); + const tagParameterContainerImage = new ecs.TagParameterContainerImage(repository); + new cdk.CfnOutput(stack, 'Output', { + value: tagParameterContainerImage.tagParameterValue, + }); + + test.throws(() => { + SynthUtils.synthesize(stack); + }, /TagParameterContainerImage must be used in a container definition when using tagParameterValue/); + + test.done(); + }, }, }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index b72151f81f2f8..3a13b8a3490a1 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -266,9 +266,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis // Only one certificate can be specified per resource, even though // `certificates` is of type Array for (let i = 0; i < additionalCerts.length; i++) { - // ids should look like: `id`, `id2`, `id3` (for backwards-compatibility) - const certId = (i > 0) ? `${id}${i + 1}` : id; - new ApplicationListenerCertificate(this, certId, { + new ApplicationListenerCertificate(this, `${id}${i + 1}`, { listener: this, certificates: [additionalCerts[i]], }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts index b6e379a17e463..e29e5a7837895 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -162,7 +162,7 @@ describe('tests', () => { ], }); - expect(listener.node.tryFindChild('DefaultCertificates')).toBeDefined(); + expect(listener.node.tryFindChild('DefaultCertificates1')).toBeDefined(); expect(listener.node.tryFindChild('DefaultCertificates2')).toBeDefined(); expect(listener.node.tryFindChild('DefaultCertificates3')).not.toBeDefined(); diff --git a/packages/@aws-cdk/aws-events-targets/README.md b/packages/@aws-cdk/aws-events-targets/README.md index cd843ef130a3a..d49621b0199cd 100644 --- a/packages/@aws-cdk/aws-events-targets/README.md +++ b/packages/@aws-cdk/aws-events-targets/README.md @@ -89,3 +89,72 @@ const rule = new events.Rule(this, 'rule', { rule.addTarget(new targets.CloudWatchLogGroup(logGroup)); ``` + +## Trigger a CodeBuild project + +Use the `CodeBuildProject` target to trigger a CodeBuild project. + +The code snippet below creates a CodeCommit repository that triggers a CodeBuild project +on commit to the master branch. You can optionally attach a +[dead letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html). + +```ts +import * as codebuild from '@aws-sdk/aws-codebuild'; +import * as codecommit from '@aws-sdk/aws-codecommit'; +import * as sqs from '@aws-sdk/aws-sqs'; +import * as targets from "@aws-cdk/aws-events-targets"; + +const repo = new codecommit.Repository(this, 'MyRepo', { + repositoryName: 'aws-cdk-codebuild-events', +}); + +const project = new codebuild.Project(this, 'MyProject', { + source: codebuild.Source.codeCommit({ repository: repo }), +}); + +const deadLetterQueue = new sqs.Queue(this, 'DeadLetterQueue'); + +// trigger a build when a commit is pushed to the repo +const onCommitRule = repo.onCommit('OnCommit', { + target: new targets.CodeBuildProject(project, { + deadLetterQueue: deadLetterQueue, + }), + branches: ['master'], +}); +``` + +## Trigger a State Machine + +Use the `SfnStateMachine` target to trigger a State Machine. + +The code snippet below creates a Simple StateMachine that is triggered every minute with a +dummy object as input. +You can optionally attach a +[dead letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html) +to the target. + +```ts +import * as iam from '@aws-sdk/aws-iam'; +import * as sqs from '@aws-sdk/aws-sqs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as targets from "@aws-cdk/aws-events-targets"; + +const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), +}); + +const dlq = new sqs.Queue(stack, 'DeadLetterQueue'); + +const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), +}); +const stateMachine = new sfn.StateMachine(stack, 'SM', { + definition: new sfn.Wait(stack, 'Hello', { time: sfn.WaitTime.duration(cdk.Duration.seconds(10)) }), + role, +}); + +rule.addTarget(new targets.SfnStateMachine(stateMachine, { + input: events.RuleTargetInput.fromObject({ SomeParam: 'SomeValue' }), + deadLetterQueue: dlq, +})); +``` diff --git a/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts b/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts index 55eac4cadd32f..d5b5e753b3867 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts @@ -1,7 +1,8 @@ import * as codebuild from '@aws-cdk/aws-codebuild'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; -import { singletonEventRole } from './util'; +import * as sqs from '@aws-cdk/aws-sqs'; +import { addToDeadLetterQueueResourcePolicy, singletonEventRole } from './util'; /** * Customize the CodeBuild Event Target @@ -24,6 +25,18 @@ export interface CodeBuildProjectProps { * @default - the entire EventBridge event */ readonly event?: events.RuleTargetInput; + + /** + * The SQS queue to be used as deadLetterQueue. + * Check out the [considerations for using a dead-letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html#dlq-considerations). + * + * The events not successfully delivered are automatically retried for a specified period of time, + * depending on the retry policy of the target. + * If an event is not delivered before all retry attempts are exhausted, it will be sent to the dead letter queue. + * + * @default - no dead-letter queue + */ + readonly deadLetterQueue?: sqs.IQueue; } /** @@ -39,9 +52,15 @@ export class CodeBuildProject implements events.IRuleTarget { * Allows using build projects as event rule targets. */ public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig { + + if (this.props.deadLetterQueue) { + addToDeadLetterQueueResourcePolicy(_rule, this.props.deadLetterQueue); + } + return { id: '', arn: this.project.projectArn, + deadLetterConfig: this.props.deadLetterQueue ? { arn: this.props.deadLetterQueue?.queueArn } : undefined, role: this.props.eventRole || singletonEventRole(this.project, [ new iam.PolicyStatement({ actions: ['codebuild:StartBuild'], diff --git a/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts b/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts index acffea0eea6f4..a2a791715e94c 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts @@ -1,7 +1,8 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; +import * as sqs from '@aws-cdk/aws-sqs'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { singletonEventRole } from './util'; +import { addToDeadLetterQueueResourcePolicy, singletonEventRole } from './util'; /** * Customize the Step Functions State Machine target @@ -20,6 +21,18 @@ export interface SfnStateMachineProps { * @default - a new role will be created */ readonly role?: iam.IRole; + + /** + * The SQS queue to be used as deadLetterQueue. + * Check out the [considerations for using a dead-letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html#dlq-considerations). + * + * The events not successfully delivered are automatically retried for a specified period of time, + * depending on the retry policy of the target. + * If an event is not delivered before all retry attempts are exhausted, it will be sent to the dead letter queue. + * + * @default - no dead-letter queue + */ + readonly deadLetterQueue?: sqs.IQueue; } /** @@ -43,9 +56,14 @@ export class SfnStateMachine implements events.IRuleTarget { * @see https://docs.aws.amazon.com/eventbridge/latest/userguide/resource-based-policies-eventbridge.html#sns-permissions */ public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig { + if (this.props.deadLetterQueue) { + addToDeadLetterQueueResourcePolicy(_rule, this.props.deadLetterQueue); + } + return { id: '', arn: this.machine.stateMachineArn, + deadLetterConfig: this.props.deadLetterQueue ? { arn: this.props.deadLetterQueue?.queueArn } : undefined, role: this.role, input: this.props.input, targetResource: this.machine, diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts index c7bb3d59b68e8..1eba641d7c8ce 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts @@ -2,6 +2,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; +import * as sqs from '@aws-cdk/aws-sqs'; import { CfnElement, Stack } from '@aws-cdk/core'; import * as targets from '../../lib'; @@ -120,4 +121,84 @@ describe('CodeBuild event target', () => { ], })); }); + + test('use a Dead Letter Queue for the rule target', () => { + // GIVEN + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 hour)'), + }); + + const queue = new sqs.Queue(stack, 'Queue'); + + // WHEN + const eventInput = { + buildspecOverride: 'buildspecs/hourly.yml', + }; + + rule.addTarget( + new targets.CodeBuildProject(project, { + event: events.RuleTargetInput.fromObject(eventInput), + deadLetterQueue: queue, + }), + ); + + // THEN + expect(stack).to(haveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: projectArn, + Id: 'Target0', + DeadLetterConfig: { + Arn: { + 'Fn::GetAtt': [ + 'Queue4A7E3555', + 'Arn', + ], + }, + }, + Input: JSON.stringify(eventInput), + RoleArn: { + 'Fn::GetAtt': ['MyProjectEventsRole5B7D93F5', 'Arn'], + }, + }, + ], + })); + + expect(stack).to(haveResource('AWS::SQS::QueuePolicy', { + PolicyDocument: { + Statement: [ + { + Action: 'sqs:SendMessage', + Condition: { + ArnEquals: { + 'aws:SourceArn': { + 'Fn::GetAtt': [ + 'Rule4C995B7F', + 'Arn', + ], + }, + }, + }, + Effect: 'Allow', + Principal: { + Service: 'events.amazonaws.com', + }, + Resource: { + 'Fn::GetAtt': [ + 'Queue4A7E3555', + 'Arn', + ], + }, + Sid: 'AllowEventRuleRule', + }, + ], + Version: '2012-10-17', + }, + Queues: [ + { + Ref: 'Queue4A7E3555', + }, + ], + })); + }); }); diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json index 007503de08383..aec41898a4622 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json @@ -43,6 +43,14 @@ "Arn" ] }, + "DeadLetterConfig": { + "Arn": { + "Fn::GetAtt": [ + "DeadLetterQueue9F481546", + "Arn" + ] + } + }, "Id": "Target0", "RoleArn": { "Fn::GetAtt": [ @@ -394,6 +402,50 @@ } } }, + "DeadLetterQueue9F481546": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DeadLetterQueuePolicyB1FB890C": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Fn::GetAtt": [ + "MyRepoOnCommit0E80B304", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "DeadLetterQueue9F481546", + "Arn" + ] + }, + "Sid": "AllowEventRuleawscdkcodebuildeventsMyRepoOnCommit0ED1137A" + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "DeadLetterQueue9F481546" + } + ] + } + }, "MyTopic86869434": { "Type": "AWS::SNS::Topic" }, diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts index 6d17e0e01c688..7974a4188f969 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts @@ -20,6 +20,7 @@ const project = new codebuild.Project(stack, 'MyProject', { }); const queue = new sqs.Queue(stack, 'MyQueue'); +const deadLetterQueue = new sqs.Queue(stack, 'DeadLetterQueue'); const topic = new sns.Topic(stack, 'MyTopic'); topic.addSubscription(new subs.SqsSubscription(queue)); @@ -39,7 +40,9 @@ project.onPhaseChange('PhaseChange', { // trigger a build when a commit is pushed to the repo const onCommitRule = repo.onCommit('OnCommit', { - target: new targets.CodeBuildProject(project), + target: new targets.CodeBuildProject(project, { + deadLetterQueue: deadLetterQueue, + }), branches: ['master'], }); diff --git a/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts b/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts index 3b50ef569f004..9dc3b2d0a4e83 100644 --- a/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts @@ -1,6 +1,7 @@ import '@aws-cdk/assert/jest'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; +import * as sqs from '@aws-cdk/aws-sqs'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as targets from '../../lib'; @@ -110,3 +111,95 @@ test('Existing role can be used for State machine Rule target', () => { }, }); }); + +test('use a Dead Letter Queue for the rule target', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + const dlq = new sqs.Queue(stack, 'DeadLetterQueue'); + + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), + }); + const stateMachine = new sfn.StateMachine(stack, 'SM', { + definition: new sfn.Wait(stack, 'Hello', { time: sfn.WaitTime.duration(cdk.Duration.seconds(10)) }), + role, + }); + + // WHEN + rule.addTarget(new targets.SfnStateMachine(stateMachine, { + input: events.RuleTargetInput.fromObject({ SomeParam: 'SomeValue' }), + deadLetterQueue: dlq, + })); + + // the Permission resource should be in the event stack + expect(stack).toHaveResource('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 minute)', + State: 'ENABLED', + Targets: [ + { + Arn: { + Ref: 'SM934E715A', + }, + DeadLetterConfig: { + Arn: { + 'Fn::GetAtt': [ + 'DeadLetterQueue9F481546', + 'Arn', + ], + }, + }, + Id: 'Target0', + Input: '{"SomeParam":"SomeValue"}', + RoleArn: { + 'Fn::GetAtt': [ + 'SMEventsRoleB320A902', + 'Arn', + ], + }, + }, + ], + }); + + expect(stack).toHaveResource('AWS::SQS::QueuePolicy', { + PolicyDocument: { + Statement: [ + { + Action: 'sqs:SendMessage', + Condition: { + ArnEquals: { + 'aws:SourceArn': { + 'Fn::GetAtt': [ + 'Rule4C995B7F', + 'Arn', + ], + }, + }, + }, + Effect: 'Allow', + Principal: { + Service: 'events.amazonaws.com', + }, + Resource: { + 'Fn::GetAtt': [ + 'DeadLetterQueue9F481546', + 'Arn', + ], + }, + Sid: 'AllowEventRuleStackRuleF6E31DD0', + }, + ], + Version: '2012-10-17', + }, + Queues: [ + { + Ref: 'DeadLetterQueue9F481546', + }, + ], + }); +}); diff --git a/packages/@aws-cdk/aws-events/README.md b/packages/@aws-cdk/aws-events/README.md index 565b0537d091d..bb0e434837131 100644 --- a/packages/@aws-cdk/aws-events/README.md +++ b/packages/@aws-cdk/aws-events/README.md @@ -83,6 +83,7 @@ onCommitRule.addTarget(new targets.SnsTopic(topic, { ## Scheduling You can configure a Rule to run on a schedule (cron or rate). +Rate must be specified in minutes, hours or days. The following example runs a task every day at 4am: @@ -186,3 +187,17 @@ bus.archive('MyArchive', { retention: cdk.Duration.days(365), }); ``` + +## Granting PutEvents to an existing EventBus + +To import an existing EventBus into your CDK application, use `EventBus.fromEventBusArn` or `EventBus.fromEventBusAttributes` +factory method. + +Then, you can use the `grantPutEventsTo` method to grant `event:PutEvents` to the eventBus. + +```ts +const eventBus = EventBus.fromEventBusArn(this, 'ImportedEventBus', 'arn:aws:events:us-east-1:111111111:event-bus/my-event-bus'); + +// now you can just call methods on the eventbus +eventBus.grantPutEventsTo(lambdaFunction); +``` diff --git a/packages/@aws-cdk/aws-events/lib/event-bus.ts b/packages/@aws-cdk/aws-events/lib/event-bus.ts index cd0c7f913cbf6..2ea9e2300163c 100644 --- a/packages/@aws-cdk/aws-events/lib/event-bus.ts +++ b/packages/@aws-cdk/aws-events/lib/event-bus.ts @@ -47,6 +47,14 @@ export interface IEventBus extends IResource { * @param props Properties of the archive */ archive(id: string, props: BaseArchiveProps): Archive; + + /** + * Grants an IAM Principal to send custom events to the eventBus + * so that they can be matched to rules. + * + * @param grantee The principal (no-op if undefined) + */ + grantPutEventsTo(grantee: iam.IGrantable): iam.Grant; } /** @@ -137,6 +145,14 @@ abstract class EventBusBase extends Resource implements IEventBus { archiveName: props.archiveName, }); } + + public grantPutEventsTo(grantee: iam.IGrantable): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee, + actions: ['events:PutEvents'], + resourceArns: [this.eventBusArn], + }); + } } /** @@ -177,6 +193,7 @@ export class EventBus extends EventBusBase { * so that they can be matched to rules. * * @param grantee The principal (no-op if undefined) + * @deprecated use grantAllPutEvents instead */ public static grantPutEvents(grantee: iam.IGrantable): iam.Grant { // It's currently not possible to restrict PutEvents to specific resources. @@ -188,6 +205,20 @@ export class EventBus extends EventBusBase { }); } + /** + * Permits an IAM Principal to send custom events to EventBridge + * so that they can be matched to rules. + * + * @param grantee The principal (no-op if undefined) + */ + public static grantAllPutEvents(grantee: iam.IGrantable): iam.Grant { + return iam.Grant.addToPrincipal({ + grantee, + actions: ['events:PutEvents'], + resourceArns: ['*'], + }); + } + private static eventBusProps(defaultEventBusName: string, props?: EventBusProps) { if (props) { const { eventBusName, eventSourceName } = props; @@ -282,7 +313,11 @@ class ImportedEventBus extends EventBusBase { public readonly eventBusPolicy: string; public readonly eventSourceName?: string; constructor(scope: Construct, id: string, attrs: EventBusAttributes) { - super(scope, id); + const arnParts = Stack.of(scope).parseArn(attrs.eventBusArn); + super(scope, id, { + account: arnParts.account, + region: arnParts.region, + }); this.eventBusArn = attrs.eventBusArn; this.eventBusName = attrs.eventBusName; diff --git a/packages/@aws-cdk/aws-events/lib/schedule.ts b/packages/@aws-cdk/aws-events/lib/schedule.ts index 8c2c04481c0d2..c12afde30aa01 100644 --- a/packages/@aws-cdk/aws-events/lib/schedule.ts +++ b/packages/@aws-cdk/aws-events/lib/schedule.ts @@ -17,6 +17,13 @@ export abstract class Schedule { * Construct a schedule from an interval and a time unit */ public static rate(duration: Duration): Schedule { + const validDurationUnit = ['minute', 'minutes', 'hour', 'hours', 'day', 'days']; + if (validDurationUnit.indexOf(duration.unitLabel()) === -1) { + throw new Error('Allowed unit for scheduling is: \'minute\', \'minutes\', \'hour\', \'hours\', \'day\', \'days\''); + } + if (duration.isUnresolved()) { + return new LiteralSchedule(`rate(${duration.formatTokenToNumber()})`); + } if (duration.toSeconds() === 0) { throw new Error('Duration cannot be 0'); } diff --git a/packages/@aws-cdk/aws-events/test/test.event-bus.ts b/packages/@aws-cdk/aws-events/test/test.event-bus.ts index 2e8434e147bb1..2babb3f9b659e 100644 --- a/packages/@aws-cdk/aws-events/test/test.event-bus.ts +++ b/packages/@aws-cdk/aws-events/test/test.event-bus.ts @@ -1,6 +1,6 @@ import { expect, haveResource } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; -import { Aws, CfnResource, Stack } from '@aws-cdk/core'; +import { Aws, CfnResource, Stack, Arn } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import { EventBus } from '../lib'; @@ -55,6 +55,65 @@ export = { test.done(); }, + 'imported event bus'(test: Test) { + const stack = new Stack(); + + const eventBus = new EventBus(stack, 'Bus'); + + const importEB = EventBus.fromEventBusArn(stack, 'ImportBus', eventBus.eventBusArn); + + // WHEN + new CfnResource(stack, 'Res', { + type: 'Test::Resource', + properties: { + EventBusArn1: eventBus.eventBusArn, + EventBusArn2: importEB.eventBusArn, + }, + }); + + expect(stack).to(haveResource('Test::Resource', { + EventBusArn1: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, + EventBusArn2: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, + })); + + test.done(); + }, + + 'same account imported event bus has right resource env'(test: Test) { + const stack = new Stack(); + + const eventBus = new EventBus(stack, 'Bus'); + + const importEB = EventBus.fromEventBusArn(stack, 'ImportBus', eventBus.eventBusArn); + + // WHEN + test.deepEqual(stack.resolve(importEB.env.account), { 'Fn::Select': [4, { 'Fn::Split': [':', { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }] }] }); + test.deepEqual(stack.resolve(importEB.env.region), { 'Fn::Select': [3, { 'Fn::Split': [':', { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }] }] }); + + test.done(); + }, + + 'cross account imported event bus has right resource env'(test: Test) { + const stack = new Stack(); + + const arnParts = { + resource: 'bus', + service: 'events', + account: 'myAccount', + region: 'us-west-1', + }; + + const arn = Arn.format(arnParts, stack); + + const importEB = EventBus.fromEventBusArn(stack, 'ImportBus', arn); + + // WHEN + test.deepEqual(importEB.env.account, arnParts.account); + test.deepEqual(importEB.env.region, arnParts.region); + + test.done(); + }, + 'can get bus name'(test: Test) { // GIVEN const stack = new Stack(); @@ -247,6 +306,76 @@ export = { test.done(); }, + + 'can grant PutEvents using grantAllPutEvents'(test: Test) { + // GIVEN + const stack = new Stack(); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + // WHEN + EventBus.grantAllPutEvents(role); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'events:PutEvents', + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + Roles: [ + { + Ref: 'Role1ABCC5F0', + }, + ], + })); + + test.done(); + }, + 'can grant PutEvents to a specific event bus'(test: Test) { + // GIVEN + const stack = new Stack(); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + + const eventBus = new EventBus(stack, 'EventBus'); + + // WHEN + eventBus.grantPutEventsTo(role); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'events:PutEvents', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'EventBus7B8748AA', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + Roles: [ + { + Ref: 'Role1ABCC5F0', + }, + ], + })); + + test.done(); + }, 'can archive events'(test: Test) { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index 80cf91948f5fa..72fe340242924 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -49,6 +49,39 @@ export = { test.done(); }, + 'get rate as token'(test: Test) { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'MyScheduledStack'); + const lazyDuration = cdk.Duration.minutes(cdk.Lazy.number({ produce: () => 5 })); + + new Rule(stack, 'MyScheduledRule', { + ruleName: 'rateInMinutes', + schedule: Schedule.rate(lazyDuration), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + 'Name': 'rateInMinutes', + 'ScheduleExpression': 'rate(5 minutes)', + })); + + test.done(); + }, + + 'Seconds is not an allowed value for Schedule rate'(test: Test) { + const lazyDuration = cdk.Duration.seconds(cdk.Lazy.number({ produce: () => 5 })); + test.throws(() => Schedule.rate(lazyDuration), /Allowed unit for scheduling is: 'minute', 'minutes', 'hour', 'hours', 'day', 'days'/); + test.done(); + }, + + 'Millis is not an allowed value for Schedule rate'(test: Test) { + const lazyDuration = cdk.Duration.millis(cdk.Lazy.number({ produce: () => 5 })); + + // THEN + test.throws(() => Schedule.rate(lazyDuration), /Allowed unit for scheduling is: 'minute', 'minutes', 'hour', 'hours', 'day', 'days'/); + test.done(); + }, + 'rule with physical name'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-events/test/test.schedule.ts b/packages/@aws-cdk/aws-events/test/test.schedule.ts index d2e045199360f..e9c24a51c92b2 100644 --- a/packages/@aws-cdk/aws-events/test/test.schedule.ts +++ b/packages/@aws-cdk/aws-events/test/test.schedule.ts @@ -1,4 +1,4 @@ -import { Duration } from '@aws-cdk/core'; +import { Duration, Stack, Lazy } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as events from '../lib'; @@ -33,8 +33,15 @@ export = { 'rate must be whole number of minutes'(test: Test) { test.throws(() => { - events.Schedule.rate(Duration.seconds(12345)); - }, /'12345 seconds' cannot be converted into a whole number of minutes/); + events.Schedule.rate(Duration.minutes(0.13456)); + }, /'0.13456 minutes' cannot be converted into a whole number of seconds/); + test.done(); + }, + + 'rate must be whole number'(test: Test) { + test.throws(() => { + events.Schedule.rate(Duration.minutes(1/8)); + }, /'0.125 minutes' cannot be converted into a whole number of seconds/); test.done(); }, @@ -44,4 +51,33 @@ export = { }, /Duration cannot be 0/); test.done(); }, + + 'rate can be from a token'(test: Test) { + const stack = new Stack(); + const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 5 })); + const rate = events.Schedule.rate(lazyDuration); + test.equal('rate(5 minutes)', stack.resolve(rate).expressionString); + test.done(); + }, + + 'rate can be in minutes'(test: Test) { + test.equal('rate(10 minutes)', + events.Schedule.rate(Duration.minutes(10)) + .expressionString); + test.done(); + }, + + 'rate can be in days'(test: Test) { + test.equal('rate(10 days)', + events.Schedule.rate(Duration.days(10)) + .expressionString); + test.done(); + }, + + 'rate can be in hours'(test: Test) { + test.equal('rate(10 hours)', + events.Schedule.rate(Duration.hours(10)) + .expressionString); + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-iam/README.md b/packages/@aws-cdk/aws-iam/README.md index f2b86ccff2f59..8cf5978aaa7c6 100644 --- a/packages/@aws-cdk/aws-iam/README.md +++ b/packages/@aws-cdk/aws-iam/README.md @@ -364,6 +364,47 @@ const provider = new iam.OpenIdConnectProvider(this, 'MyProvider', { const principal = new iam.OpenIdConnectPrincipal(provider); ``` +## SAML provider + +An IAM SAML 2.0 identity provider is an entity in IAM that describes an external +identity provider (IdP) service that supports the SAML 2.0 (Security Assertion +Markup Language 2.0) standard. You use an IAM identity provider when you want +to establish trust between a SAML-compatible IdP such as Shibboleth or Active +Directory Federation Services and AWS, so that users in your organization can +access AWS resources. IAM SAML identity providers are used as principals in an +IAM trust policy. + +```ts +new iam.SamlProvider(this, 'Provider', { + metadataDocument: iam.SamlMetadataDocument.fromFile('/path/to/saml-metadata-document.xml'), +}); +``` + +The `SamlPrincipal` class can be used as a principal with a `SamlProvider`: + +```ts +const provider = new iam.SamlProvider(this, 'Provider', { + metadataDocument: iam.SamlMetadataDocument.fromFile('/path/to/saml-metadata-document.xml'), +}); +const principal = new iam.SamlPrincipal(provider, { + StringEquals: { + 'SAML:iss': 'issuer', + }, +}); +``` + +When creating a role for programmatic and AWS Management Console access, use the `SamlConsolePrincipal` +class: + +```ts +const provider = new iam.SamlProvider(this, 'Provider', { + metadataDocument: iam.SamlMetadataDocument.fromFile('/path/to/saml-metadata-document.xml'), +}); +new iam.Role(this, 'Role', { + assumedBy: new iam.SamlConsolePrincipal(provider), +}); +``` + ## Users IAM manages users for your AWS account. To create a new user: diff --git a/packages/@aws-cdk/aws-iam/lib/index.ts b/packages/@aws-cdk/aws-iam/lib/index.ts index 19b8a156ba598..06c2a9bb6cdcd 100644 --- a/packages/@aws-cdk/aws-iam/lib/index.ts +++ b/packages/@aws-cdk/aws-iam/lib/index.ts @@ -12,6 +12,7 @@ export * from './grant'; export * from './unknown-principal'; export * from './oidc-provider'; export * from './permissions-boundary'; +export * from './saml-provider'; // AWS::IAM CloudFormation Resources: export * from './iam.generated'; diff --git a/packages/@aws-cdk/aws-iam/lib/oidc-provider/external.ts b/packages/@aws-cdk/aws-iam/lib/oidc-provider/external.ts index d926caf405e22..4ad18aed4f17d 100644 --- a/packages/@aws-cdk/aws-iam/lib/oidc-provider/external.ts +++ b/packages/@aws-cdk/aws-iam/lib/oidc-provider/external.ts @@ -28,7 +28,7 @@ async function downloadThumbprint(issuerUrl: string) { if (!purl.host) { return ko(new Error(`unable to determine host from issuer url ${issuerUrl}`)); } - const socket = tls.connect(port, purl.host, { rejectUnauthorized: false }); + const socket = tls.connect(port, purl.host, { rejectUnauthorized: false, servername: purl.host }); socket.once('error', ko); socket.once('secureConnect', () => { const cert = socket.getPeerCertificate(); diff --git a/packages/@aws-cdk/aws-iam/lib/principals.ts b/packages/@aws-cdk/aws-iam/lib/principals.ts index ee4cdf0c43e27..6295b8fa966e9 100644 --- a/packages/@aws-cdk/aws-iam/lib/principals.ts +++ b/packages/@aws-cdk/aws-iam/lib/principals.ts @@ -2,6 +2,7 @@ import * as cdk from '@aws-cdk/core'; import { Default, RegionInfo } from '@aws-cdk/region-info'; import { IOpenIdConnectProvider } from './oidc-provider'; import { Condition, Conditions, PolicyStatement } from './policy-statement'; +import { ISamlProvider } from './saml-provider'; import { mergePrincipal } from './util'; /** @@ -493,6 +494,38 @@ export class OpenIdConnectPrincipal extends WebIdentityPrincipal { } } +/** + * Principal entity that represents a SAML federated identity provider + */ +export class SamlPrincipal extends FederatedPrincipal { + constructor(samlProvider: ISamlProvider, conditions: Conditions) { + super(samlProvider.samlProviderArn, conditions, 'sts:AssumeRoleWithSAML'); + } + + public toString() { + return `SamlPrincipal(${this.federated})`; + } +} + +/** + * Principal entity that represents a SAML federated identity provider for + * programmatic and AWS Management Console access. + */ +export class SamlConsolePrincipal extends SamlPrincipal { + constructor(samlProvider: ISamlProvider, conditions: Conditions = {}) { + super(samlProvider, { + ...conditions, + StringEquals: { + 'SAML:aud': 'https://signin.aws.amazon.com/saml', + }, + }); + } + + public toString() { + return `SamlConsolePrincipal(${this.federated})`; + } +} + /** * Use the AWS account into which a stack is deployed as the principal entity in a policy */ diff --git a/packages/@aws-cdk/aws-iam/lib/saml-provider.ts b/packages/@aws-cdk/aws-iam/lib/saml-provider.ts new file mode 100644 index 0000000000000..33712adb60ed2 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/lib/saml-provider.ts @@ -0,0 +1,100 @@ +import * as fs from 'fs'; +import { IResource, Resource, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnSAMLProvider } from './iam.generated'; + +/** + * A SAML provider + */ +export interface ISamlProvider extends IResource { + /** + * The Amazon Resource Name (ARN) of the provider + * + * @attribute + */ + readonly samlProviderArn: string; +} + +/** + * Properties for a SAML provider + */ +export interface SamlProviderProps { + /** + * The name of the provider to create. + * + * This parameter allows a string of characters consisting of upper and + * lowercase alphanumeric characters with no spaces. You can also include + * any of the following characters: _+=,.@- + * + * Length must be between 1 and 128 characters. + * + * @default - a CloudFormation generated name + */ + readonly name?: string; + + /** + * An XML document generated by an identity provider (IdP) that supports + * SAML 2.0. The document includes the issuer's name, expiration information, + * and keys that can be used to validate the SAML authentication response + * (assertions) that are received from the IdP. You must generate the metadata + * document using the identity management software that is used as your + * organization's IdP. + */ + readonly metadataDocument: SamlMetadataDocument; +} + +/** + * A SAML metadata document + */ +export abstract class SamlMetadataDocument { + /** + * Create a SAML metadata document from a XML string + */ + public static fromXml(xml: string): SamlMetadataDocument { + return { xml }; + } + + /** + * Create a SAML metadata document from a XML file + */ + public static fromFile(path: string): SamlMetadataDocument { + return { xml: fs.readFileSync(path, 'utf-8') }; + } + + /** + * The XML content of the metadata document + */ + public abstract readonly xml: string; +} + +/** + * A SAML provider + */ +export class SamlProvider extends Resource implements ISamlProvider { + /** + * Import an existing provider + */ + public static fromSamlProviderArn(scope: Construct, id: string, samlProviderArn: string): ISamlProvider { + class Import extends Resource implements ISamlProvider { + public readonly samlProviderArn = samlProviderArn; + } + return new Import(scope, id); + } + + public readonly samlProviderArn: string; + + constructor(scope: Construct, id: string, props: SamlProviderProps) { + super(scope, id); + + if (props.name && !Token.isUnresolved(props.name) && !/^[\w+=,.@-]{1,128}$/.test(props.name)) { + throw new Error('Invalid SAML provider name. The name must be a string of characters consisting of upper and lowercase alphanumeric characters with no spaces. You can also include any of the following characters: _+=,.@-. Length must be between 1 and 128 characters.'); + } + + const samlProvider = new CfnSAMLProvider(this, 'Resource', { + name: this.physicalName, + samlMetadataDocument: props.metadataDocument.xml, + }); + + this.samlProviderArn = samlProvider.ref; + } +} diff --git a/packages/@aws-cdk/aws-iam/package.json b/packages/@aws-cdk/aws-iam/package.json index 6455688b5c006..76cd09b7b2d9a 100644 --- a/packages/@aws-cdk/aws-iam/package.json +++ b/packages/@aws-cdk/aws-iam/package.json @@ -105,6 +105,7 @@ "from-signature:@aws-cdk/aws-iam.Role.fromRoleArn", "construct-interface-extends-iconstruct:@aws-cdk/aws-iam.IManagedPolicy", "props-physical-name:@aws-cdk/aws-iam.OpenIdConnectProviderProps", + "props-physical-name:@aws-cdk/aws-iam.SamlProviderProps", "resource-interface-extends-resource:@aws-cdk/aws-iam.IManagedPolicy", "docs-public-apis:@aws-cdk/aws-iam.IUser" ] diff --git a/packages/@aws-cdk/aws-iam/test/integ.saml-provider.expected.json b/packages/@aws-cdk/aws-iam/test/integ.saml-provider.expected.json new file mode 100644 index 0000000000000..6a5263ebc9675 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/integ.saml-provider.expected.json @@ -0,0 +1,34 @@ +{ + "Resources": { + "Provider2281708E": { + "Type": "AWS::IAM::SAMLProvider", + "Properties": { + "SamlMetadataDocument": "\n\n \n \n \n \n \n \n \n \n \n \n xF+xF7hmYedlu04o41mAyvIFBnXuvGE368C9oNLICCA=\n \n \n cGs8ZgnhtOluTKeRZHWjLrtvP9mUxHvSpKWSM5L4MFwojXZ39HIxCAAB22VseLVn8nMH0JxEAze/SzxraCewvJmYrUYKVgECl8kaQ1AKfbWHmrqyCRm9+WX6Fsj9SEGRNOPRfVpceVZYFrw3rimgjYZq/hyjvuEsp/6Eu+2RrO/mCNT7J0y5luOXLeHwJfeNalcl1mHA0JMCusnwfQOvRjkgOKL8pvDyXti+cvicDKqExeDGTaUoUyyynNWXLBLHHUhq29ej80D6lPVZWFqAgsZPm/O3spLhWl974PzDnX0qMds3aieZbHmuots+Cdy0LXQVHLjhRbDU8F6+BU+lRQ==\n \n \n MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R\n \n \n \n \n \n \n \n MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R\n \n \n \n \n \n \n MIIDBTCCAe2gAwIBAgIQWPB1ofOpA7FFlOBk5iPaNTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIxMDIwNzE3MDAzOVoXDTI2MDIwNjE3MDAzOVowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALH7FzF1rjvnZ4i2iBC2tz8qs/WP61n3/wFawgJxUnTx2vP/z5pG7f8qvumd7taOII0aSlp648SIfMw59WdUUtup5CnDYOcX1sUdivAj20m2PIDK6f+KWZ+7YKxJqCzJMH4GGlQvuDIhRKNT9oHfZgnYCCAmjXmJBtWyD052qqrkzOSn0/e9TKbjlTnTNcrIno3XDQ7xG+79vOD2GZMNopsKogWNxUdLFRu44ClKLRb4Xe00eVrANtBkv+mSJFFJS1Gxv611hpdGI2S0v1H+KvB26O7vuzGhZ/AevRemGhXQ5V5vwNEqXnVRVkBRszLKeN/+rxM436xQyVQGJMG+sVECAwEAAaMhMB8wHQYDVR0OBBYEFLlRBSxxgmNPObCFrl+hSsbcvRkcMA0GCSqGSIb3DQEBCwUAA4IBAQB+UQFTNs6BUY3AIGkS2ZRuZgJsNEr/ZEM4aCs2domd2Oqj7+5iWsnPh5CugFnI4nd+ZLgKVHSD6acQ27we+eNY6gxfpQCY1fiN/uKOOsA0If8IbPdBEhtPerRgPJFXLHaYVqD8UYDo5KNCcoB4Kh8nvCWRGPUUHPRqp7AnAcVrcbiXA/bmMCnFWuNNahcaAKiJTxYlKDaDIiPN35yECYbDj0PBWJUxobrvj5I275jbikkp8QSLYnSU/v7dMDUbxSLfZ7zsTuaF2Qx+L62PsYTwLzIFX3M8EMSQ6h68TupFTi5n0M2yIXQgoRoNEDWNJZ/aZMY/gqT02GQGBWrh+/vJ\n \n \n \n \n \n Name\n The mutable display name of the user.\n \n \n Subject\n An immutable, globally unique, non-reusable identifier of the user that is unique to the application for which a token is issued.\n \n \n Given Name\n First name of the user.\n \n \n Surname\n Last name of the user.\n \n \n Display Name\n Display name of the user.\n \n \n Nick Name\n Nick name of the user.\n \n \n Authentication Instant\n The time (UTC) when the user is authenticated to Windows Azure Active Directory.\n \n \n Authentication Method\n The method that Windows Azure Active Directory uses to authenticate users.\n \n \n ObjectIdentifier\n Primary identifier for the user in the directory. Immutable, globally unique, non-reusable.\n \n \n TenantId\n Identifier for the user's tenant.\n \n \n IdentityProvider\n Identity provider for the user.\n \n \n Email\n Email address of the user.\n \n \n Groups\n Groups of the user.\n \n \n External Access Token\n Access token issued by external identity provider.\n \n \n External Access Token Expiration\n UTC expiration time of access token issued by external identity provider.\n \n \n External OpenID 2.0 Identifier\n OpenID 2.0 identifier issued by external identity provider.\n \n \n GroupsOverageClaim\n Issued when number of user's group claims exceeds return limit.\n \n \n Role Claim\n Roles that the user or Service Principal is attached to\n \n \n RoleTemplate Id Claim\n Role template id of the Built-in Directory Roles that the user is a member of\n \n \n \n \n https://login.microsoftonline.com/9ecb31cb-702e-4ce0-ae22-bcee28d49d49/wsfed\n \n \n \n \n https://login.microsoftonline.com/9ecb31cb-702e-4ce0-ae22-bcee28d49d49/wsfed\n \n \n \n \n \n \n \n MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R\n \n \n \n \n \n \n MIIDBTCCAe2gAwIBAgIQWPB1ofOpA7FFlOBk5iPaNTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIxMDIwNzE3MDAzOVoXDTI2MDIwNjE3MDAzOVowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALH7FzF1rjvnZ4i2iBC2tz8qs/WP61n3/wFawgJxUnTx2vP/z5pG7f8qvumd7taOII0aSlp648SIfMw59WdUUtup5CnDYOcX1sUdivAj20m2PIDK6f+KWZ+7YKxJqCzJMH4GGlQvuDIhRKNT9oHfZgnYCCAmjXmJBtWyD052qqrkzOSn0/e9TKbjlTnTNcrIno3XDQ7xG+79vOD2GZMNopsKogWNxUdLFRu44ClKLRb4Xe00eVrANtBkv+mSJFFJS1Gxv611hpdGI2S0v1H+KvB26O7vuzGhZ/AevRemGhXQ5V5vwNEqXnVRVkBRszLKeN/+rxM436xQyVQGJMG+sVECAwEAAaMhMB8wHQYDVR0OBBYEFLlRBSxxgmNPObCFrl+hSsbcvRkcMA0GCSqGSIb3DQEBCwUAA4IBAQB+UQFTNs6BUY3AIGkS2ZRuZgJsNEr/ZEM4aCs2domd2Oqj7+5iWsnPh5CugFnI4nd+ZLgKVHSD6acQ27we+eNY6gxfpQCY1fiN/uKOOsA0If8IbPdBEhtPerRgPJFXLHaYVqD8UYDo5KNCcoB4Kh8nvCWRGPUUHPRqp7AnAcVrcbiXA/bmMCnFWuNNahcaAKiJTxYlKDaDIiPN35yECYbDj0PBWJUxobrvj5I275jbikkp8QSLYnSU/v7dMDUbxSLfZ7zsTuaF2Qx+L62PsYTwLzIFX3M8EMSQ6h68TupFTi5n0M2yIXQgoRoNEDWNJZ/aZMY/gqT02GQGBWrh+/vJ\n \n \n \n \n \n https://sts.windows.net/9ecb31cb-702e-4ce0-ae22-bcee28d49d49/\n \n \n \n \n https://login.microsoftonline.com/9ecb31cb-702e-4ce0-ae22-bcee28d49d49/wsfed\n \n \n \n \n https://login.microsoftonline.com/9ecb31cb-702e-4ce0-ae22-bcee28d49d49/wsfed\n \n \n \n \n \n \n \n MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R\n \n \n \n \n \n \n MIIDBTCCAe2gAwIBAgIQWPB1ofOpA7FFlOBk5iPaNTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIxMDIwNzE3MDAzOVoXDTI2MDIwNjE3MDAzOVowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALH7FzF1rjvnZ4i2iBC2tz8qs/WP61n3/wFawgJxUnTx2vP/z5pG7f8qvumd7taOII0aSlp648SIfMw59WdUUtup5CnDYOcX1sUdivAj20m2PIDK6f+KWZ+7YKxJqCzJMH4GGlQvuDIhRKNT9oHfZgnYCCAmjXmJBtWyD052qqrkzOSn0/e9TKbjlTnTNcrIno3XDQ7xG+79vOD2GZMNopsKogWNxUdLFRu44ClKLRb4Xe00eVrANtBkv+mSJFFJS1Gxv611hpdGI2S0v1H+KvB26O7vuzGhZ/AevRemGhXQ5V5vwNEqXnVRVkBRszLKeN/+rxM436xQyVQGJMG+sVECAwEAAaMhMB8wHQYDVR0OBBYEFLlRBSxxgmNPObCFrl+hSsbcvRkcMA0GCSqGSIb3DQEBCwUAA4IBAQB+UQFTNs6BUY3AIGkS2ZRuZgJsNEr/ZEM4aCs2domd2Oqj7+5iWsnPh5CugFnI4nd+ZLgKVHSD6acQ27we+eNY6gxfpQCY1fiN/uKOOsA0If8IbPdBEhtPerRgPJFXLHaYVqD8UYDo5KNCcoB4Kh8nvCWRGPUUHPRqp7AnAcVrcbiXA/bmMCnFWuNNahcaAKiJTxYlKDaDIiPN35yECYbDj0PBWJUxobrvj5I275jbikkp8QSLYnSU/v7dMDUbxSLfZ7zsTuaF2Qx+L62PsYTwLzIFX3M8EMSQ6h68TupFTi5n0M2yIXQgoRoNEDWNJZ/aZMY/gqT02GQGBWrh+/vJ\n \n \n \n \n \n \n \n\n" + } + }, + "Role1ABCC5F0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithSAML", + "Condition": { + "StringEquals": { + "SAML:aud": "https://signin.aws.amazon.com/saml" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": { + "Ref": "Provider2281708E" + } + } + } + ], + "Version": "2012-10-17" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.saml-provider.ts b/packages/@aws-cdk/aws-iam/test/integ.saml-provider.ts new file mode 100644 index 0000000000000..e421b8c4d2b01 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/integ.saml-provider.ts @@ -0,0 +1,22 @@ +import * as path from 'path'; +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as iam from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const provider = new iam.SamlProvider(this, 'Provider', { + metadataDocument: iam.SamlMetadataDocument.fromFile(path.join(__dirname, 'saml-metadata-document.xml')), + }); + + new iam.Role(this, 'Role', { + assumedBy: new iam.SamlConsolePrincipal(provider), + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-saml-provider'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iam/test/principals.test.ts b/packages/@aws-cdk/aws-iam/test/principals.test.ts index 115430ed58552..43ed8433c47f6 100644 --- a/packages/@aws-cdk/aws-iam/test/principals.test.ts +++ b/packages/@aws-cdk/aws-iam/test/principals.test.ts @@ -127,4 +127,42 @@ test('use OpenID Connect principal from provider', () => { // THEN expect(stack.resolve(principal.federated)).toStrictEqual({ Ref: 'MyProvider730BA1C8' }); -}); \ No newline at end of file +}); + +test('SAML principal', () => { + // GIVEN + const stack = new Stack(); + const provider = new iam.SamlProvider(stack, 'MyProvider', { + metadataDocument: iam.SamlMetadataDocument.fromXml('document'), + }); + + // WHEN + const principal = new iam.SamlConsolePrincipal(provider); + new iam.Role(stack, 'Role', { + assumedBy: principal, + }); + + // THEN + expect(stack.resolve(principal.federated)).toStrictEqual({ Ref: 'MyProvider730BA1C8' }); + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRoleWithSAML', + Condition: { + StringEquals: { + 'SAML:aud': 'https://signin.aws.amazon.com/saml', + }, + }, + Effect: 'Allow', + Principal: { + Federated: { + Ref: 'MyProvider730BA1C8', + }, + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); diff --git a/packages/@aws-cdk/aws-iam/test/saml-metadata-document.xml b/packages/@aws-cdk/aws-iam/test/saml-metadata-document.xml new file mode 100644 index 0000000000000..a53ae7763165d --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/saml-metadata-document.xml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + xF+xF7hmYedlu04o41mAyvIFBnXuvGE368C9oNLICCA= + + + cGs8ZgnhtOluTKeRZHWjLrtvP9mUxHvSpKWSM5L4MFwojXZ39HIxCAAB22VseLVn8nMH0JxEAze/SzxraCewvJmYrUYKVgECl8kaQ1AKfbWHmrqyCRm9+WX6Fsj9SEGRNOPRfVpceVZYFrw3rimgjYZq/hyjvuEsp/6Eu+2RrO/mCNT7J0y5luOXLeHwJfeNalcl1mHA0JMCusnwfQOvRjkgOKL8pvDyXti+cvicDKqExeDGTaUoUyyynNWXLBLHHUhq29ej80D6lPVZWFqAgsZPm/O3spLhWl974PzDnX0qMds3aieZbHmuots+Cdy0LXQVHLjhRbDU8F6+BU+lRQ== + + + MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R + + + + + + + + MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R + + + + + + + MIIDBTCCAe2gAwIBAgIQWPB1ofOpA7FFlOBk5iPaNTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIxMDIwNzE3MDAzOVoXDTI2MDIwNjE3MDAzOVowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALH7FzF1rjvnZ4i2iBC2tz8qs/WP61n3/wFawgJxUnTx2vP/z5pG7f8qvumd7taOII0aSlp648SIfMw59WdUUtup5CnDYOcX1sUdivAj20m2PIDK6f+KWZ+7YKxJqCzJMH4GGlQvuDIhRKNT9oHfZgnYCCAmjXmJBtWyD052qqrkzOSn0/e9TKbjlTnTNcrIno3XDQ7xG+79vOD2GZMNopsKogWNxUdLFRu44ClKLRb4Xe00eVrANtBkv+mSJFFJS1Gxv611hpdGI2S0v1H+KvB26O7vuzGhZ/AevRemGhXQ5V5vwNEqXnVRVkBRszLKeN/+rxM436xQyVQGJMG+sVECAwEAAaMhMB8wHQYDVR0OBBYEFLlRBSxxgmNPObCFrl+hSsbcvRkcMA0GCSqGSIb3DQEBCwUAA4IBAQB+UQFTNs6BUY3AIGkS2ZRuZgJsNEr/ZEM4aCs2domd2Oqj7+5iWsnPh5CugFnI4nd+ZLgKVHSD6acQ27we+eNY6gxfpQCY1fiN/uKOOsA0If8IbPdBEhtPerRgPJFXLHaYVqD8UYDo5KNCcoB4Kh8nvCWRGPUUHPRqp7AnAcVrcbiXA/bmMCnFWuNNahcaAKiJTxYlKDaDIiPN35yECYbDj0PBWJUxobrvj5I275jbikkp8QSLYnSU/v7dMDUbxSLfZ7zsTuaF2Qx+L62PsYTwLzIFX3M8EMSQ6h68TupFTi5n0M2yIXQgoRoNEDWNJZ/aZMY/gqT02GQGBWrh+/vJ + + + + + + Name + The mutable display name of the user. + + + Subject + An immutable, globally unique, non-reusable identifier of the user that is unique to the application for which a token is issued. + + + Given Name + First name of the user. + + + Surname + Last name of the user. + + + Display Name + Display name of the user. + + + Nick Name + Nick name of the user. + + + Authentication Instant + The time (UTC) when the user is authenticated to Windows Azure Active Directory. + + + Authentication Method + The method that Windows Azure Active Directory uses to authenticate users. + + + ObjectIdentifier + Primary identifier for the user in the directory. Immutable, globally unique, non-reusable. + + + TenantId + Identifier for the user's tenant. + + + IdentityProvider + Identity provider for the user. + + + Email + Email address of the user. + + + Groups + Groups of the user. + + + External Access Token + Access token issued by external identity provider. + + + External Access Token Expiration + UTC expiration time of access token issued by external identity provider. + + + External OpenID 2.0 Identifier + OpenID 2.0 identifier issued by external identity provider. + + + GroupsOverageClaim + Issued when number of user's group claims exceeds return limit. + + + Role Claim + Roles that the user or Service Principal is attached to + + + RoleTemplate Id Claim + Role template id of the Built-in Directory Roles that the user is a member of + + + + + https://login.microsoftonline.com/9ecb31cb-702e-4ce0-ae22-bcee28d49d49/wsfed + + + + + https://login.microsoftonline.com/9ecb31cb-702e-4ce0-ae22-bcee28d49d49/wsfed + + + + + + + + MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R + + + + + + + MIIDBTCCAe2gAwIBAgIQWPB1ofOpA7FFlOBk5iPaNTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIxMDIwNzE3MDAzOVoXDTI2MDIwNjE3MDAzOVowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALH7FzF1rjvnZ4i2iBC2tz8qs/WP61n3/wFawgJxUnTx2vP/z5pG7f8qvumd7taOII0aSlp648SIfMw59WdUUtup5CnDYOcX1sUdivAj20m2PIDK6f+KWZ+7YKxJqCzJMH4GGlQvuDIhRKNT9oHfZgnYCCAmjXmJBtWyD052qqrkzOSn0/e9TKbjlTnTNcrIno3XDQ7xG+79vOD2GZMNopsKogWNxUdLFRu44ClKLRb4Xe00eVrANtBkv+mSJFFJS1Gxv611hpdGI2S0v1H+KvB26O7vuzGhZ/AevRemGhXQ5V5vwNEqXnVRVkBRszLKeN/+rxM436xQyVQGJMG+sVECAwEAAaMhMB8wHQYDVR0OBBYEFLlRBSxxgmNPObCFrl+hSsbcvRkcMA0GCSqGSIb3DQEBCwUAA4IBAQB+UQFTNs6BUY3AIGkS2ZRuZgJsNEr/ZEM4aCs2domd2Oqj7+5iWsnPh5CugFnI4nd+ZLgKVHSD6acQ27we+eNY6gxfpQCY1fiN/uKOOsA0If8IbPdBEhtPerRgPJFXLHaYVqD8UYDo5KNCcoB4Kh8nvCWRGPUUHPRqp7AnAcVrcbiXA/bmMCnFWuNNahcaAKiJTxYlKDaDIiPN35yECYbDj0PBWJUxobrvj5I275jbikkp8QSLYnSU/v7dMDUbxSLfZ7zsTuaF2Qx+L62PsYTwLzIFX3M8EMSQ6h68TupFTi5n0M2yIXQgoRoNEDWNJZ/aZMY/gqT02GQGBWrh+/vJ + + + + + + https://sts.windows.net/9ecb31cb-702e-4ce0-ae22-bcee28d49d49/ + + + + + https://login.microsoftonline.com/9ecb31cb-702e-4ce0-ae22-bcee28d49d49/wsfed + + + + + https://login.microsoftonline.com/9ecb31cb-702e-4ce0-ae22-bcee28d49d49/wsfed + + + + + + + + MIIDBTCCAe2gAwIBAgIQN33ROaIJ6bJBWDCxtmJEbjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMTIyMTIwNTAxN1oXDTI1MTIyMDIwNTAxN1owLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKGiy0/YZHEo9rRn2bI27u189Sq7NKhInFz5hLCSjgUB2rmf5ETNR3RJIDiW1M51LKROsTrjkl45cxK6gcVwLuEgr3L1TgmBtr/Rt/riKyxeXbLQ9LGBwaNVaJrSscxfdFbJa5J+qzUIFBiFoL7kE8ZtbkZJWBTxHEyEcNC52JJ8ydOhgvZYykete8AAVa2TZAbg4ECo9+6nMsaGsSBncRHJlRWVycq8Q4HV4faMEZmZ+iyCZRo2fZufXpn7sJwZ7CEBuw4qycHvUl6y153sUUFqsswnZGGjqpKSq7I7sVI9vjB199RarHaSSbDgL2FxjmASiUY4RqxnTjVa2XVHUwUCAwEAAaMhMB8wHQYDVR0OBBYEFI5mN5ftHloEDVNoIa8sQs7kJAeTMA0GCSqGSIb3DQEBCwUAA4IBAQBnaGnojxNgnV4+TCPZ9br4ox1nRn9tzY8b5pwKTW2McJTe0yEvrHyaItK8KbmeKJOBvASf+QwHkp+F2BAXzRiTl4Z+gNFQULPzsQWpmKlz6fIWhc7ksgpTkMK6AaTbwWYTfmpKnQw/KJm/6rboLDWYyKFpQcStu67RZ+aRvQz68Ev2ga5JsXlcOJ3gP/lE5WC1S0rjfabzdMOGP8qZQhXk4wBOgtFBaisDnbjV5pcIrjRPlhoCxvKgC/290nZ9/DLBH3TbHk8xwHXeBAnAjyAqOZij92uksAv7ZLq4MODcnQshVINXwsYshG1pQqOLwMertNaY5WtrubMRku44Dw7R + + + + + + + MIIDBTCCAe2gAwIBAgIQWPB1ofOpA7FFlOBk5iPaNTANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIxMDIwNzE3MDAzOVoXDTI2MDIwNjE3MDAzOVowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALH7FzF1rjvnZ4i2iBC2tz8qs/WP61n3/wFawgJxUnTx2vP/z5pG7f8qvumd7taOII0aSlp648SIfMw59WdUUtup5CnDYOcX1sUdivAj20m2PIDK6f+KWZ+7YKxJqCzJMH4GGlQvuDIhRKNT9oHfZgnYCCAmjXmJBtWyD052qqrkzOSn0/e9TKbjlTnTNcrIno3XDQ7xG+79vOD2GZMNopsKogWNxUdLFRu44ClKLRb4Xe00eVrANtBkv+mSJFFJS1Gxv611hpdGI2S0v1H+KvB26O7vuzGhZ/AevRemGhXQ5V5vwNEqXnVRVkBRszLKeN/+rxM436xQyVQGJMG+sVECAwEAAaMhMB8wHQYDVR0OBBYEFLlRBSxxgmNPObCFrl+hSsbcvRkcMA0GCSqGSIb3DQEBCwUAA4IBAQB+UQFTNs6BUY3AIGkS2ZRuZgJsNEr/ZEM4aCs2domd2Oqj7+5iWsnPh5CugFnI4nd+ZLgKVHSD6acQ27we+eNY6gxfpQCY1fiN/uKOOsA0If8IbPdBEhtPerRgPJFXLHaYVqD8UYDo5KNCcoB4Kh8nvCWRGPUUHPRqp7AnAcVrcbiXA/bmMCnFWuNNahcaAKiJTxYlKDaDIiPN35yECYbDj0PBWJUxobrvj5I275jbikkp8QSLYnSU/v7dMDUbxSLfZ7zsTuaF2Qx+L62PsYTwLzIFX3M8EMSQ6h68TupFTi5n0M2yIXQgoRoNEDWNJZ/aZMY/gqT02GQGBWrh+/vJ + + + + + + + + diff --git a/packages/@aws-cdk/aws-iam/test/saml-provider.test.ts b/packages/@aws-cdk/aws-iam/test/saml-provider.test.ts new file mode 100644 index 0000000000000..1878e65c51a14 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/saml-provider.test.ts @@ -0,0 +1,25 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { SamlMetadataDocument, SamlProvider } from '../lib'; + +let stack: Stack; +beforeEach(() => { + stack = new Stack(); +}); + +test('SAML provider', () => { + new SamlProvider(stack, 'Provider', { + metadataDocument: SamlMetadataDocument.fromXml('document'), + }); + + expect(stack).toHaveResource('AWS::IAM::SAMLProvider', { + SamlMetadataDocument: 'document', + }); +}); + +test('throws with invalid name', () => { + expect(() => new SamlProvider(stack, 'Provider', { + name: 'invalid name', + metadataDocument: SamlMetadataDocument.fromXml('document'), + })).toThrow(/Invalid SAML provider name/); +}); 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 f61d8409da7bd..9cdcc5c86a83b 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/lib/event-bridge.ts +++ b/packages/@aws-cdk/aws-lambda-destinations/lib/event-bridge.ts @@ -22,15 +22,25 @@ export class EventBridgeDestination implements lambda.IDestination { * Returns a destination configuration */ public bind(_scope: Construct, fn: lambda.IFunction, _options?: lambda.DestinationOptions): lambda.DestinationConfig { - // deduplicated automatically - events.EventBus.grantPutEvents(fn); // Cannot restrict to a specific resource + if (this.eventBus) { + this.eventBus.grantPutEventsTo(fn); + + return { + destination: this.eventBus.eventBusArn, + }; + } + + const existingDefaultEventBus = _scope.node.tryFindChild('DefaultEventBus'); + let eventBus = (existingDefaultEventBus as events.EventBus) || events.EventBus.fromEventBusArn(_scope, 'DefaultEventBus', Stack.of(fn).formatArn({ + service: 'events', + resource: 'event-bus', + resourceName: 'default', + })); + + eventBus.grantPutEventsTo(fn); return { - destination: this.eventBus && this.eventBus.eventBusArn || Stack.of(fn).formatArn({ - service: 'events', - resource: 'event-bus', - resourceName: 'default', - }), + destination: eventBus.eventBusArn, }; } } diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.test.ts b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.test.ts index 0f8f7d4c85254..70dda9ef43893 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.test.ts +++ b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.test.ts @@ -47,56 +47,12 @@ test('event bus as destination', () => { { Action: 'events:PutEvents', Effect: 'Allow', - Resource: '*', - }, - ], - Version: '2012-10-17', - }, - }); -}); - -test('event bus as destination defaults to default event bus', () => { - // WHEN - new lambda.Function(stack, 'Function', { - ...lambdaProps, - onSuccess: new destinations.EventBridgeDestination(), - }); - - // THEN - expect(stack).toHaveResource('AWS::Lambda::EventInvokeConfig', { - DestinationConfig: { - OnSuccess: { - Destination: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':events:', - { - Ref: 'AWS::Region', - }, - ':', - { - Ref: 'AWS::AccountId', - }, - ':event-bus/default', + Resource: { + 'Fn::GetAtt': [ + 'EventBus7B8748AA', + 'Arn', ], - ], - }, - }, - }, - }); - - expect(stack).toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: 'events:PutEvents', - Effect: 'Allow', - Resource: '*', + }, }, ], Version: '2012-10-17', @@ -215,7 +171,26 @@ test('lambda payload as destination', () => { { Action: 'events:PutEvents', Effect: 'Allow', - Resource: '*', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':events:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':event-bus/default', + ], + ], + }, }, ], Version: '2012-10-17', diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.expected.json b/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.expected.json index 510fe8cef21a8..009327c46da7e 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.expected.json +++ b/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.expected.json @@ -219,7 +219,26 @@ { "Action": "events:PutEvents", "Effect": "Allow", - "Resource": "*" + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":event-bus/default" + ] + ] + } }, { "Action": "lambda:InvokeFunction", diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json b/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json index 5fc64df8417f3..5fc2d65b80387 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json +++ b/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json @@ -39,7 +39,26 @@ { "Action": "events:PutEvents", "Effect": "Allow", - "Resource": "*" + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":event-bus/default" + ] + ] + } } ], "Version": "2012-10-17" @@ -289,7 +308,26 @@ { "Action": "events:PutEvents", "Effect": "Allow", - "Resource": "*" + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":event-bus/default" + ] + ] + } } ], "Version": "2012-10-17" diff --git a/packages/@aws-cdk/aws-lambda-nodejs/package.json b/packages/@aws-cdk/aws-lambda-nodejs/package.json index 7ae4a7822c113..03e6cb806817e 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/package.json +++ b/packages/@aws-cdk/aws-lambda-nodejs/package.json @@ -67,7 +67,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "delay": "5.0.0", - "esbuild": "^0.8.54", + "esbuild": "^0.8.55", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-logs/package.json b/packages/@aws-cdk/aws-logs/package.json index c1b3eef36c73b..f778b855211eb 100644 --- a/packages/@aws-cdk/aws-logs/package.json +++ b/packages/@aws-cdk/aws-logs/package.json @@ -77,7 +77,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nock": "^13.0.9", + "nock": "^13.0.10", "nodeunit": "^0.11.3", "pkglint": "0.0.0", "sinon": "^9.2.4" diff --git a/packages/@aws-cdk/aws-neptune/README.md b/packages/@aws-cdk/aws-neptune/README.md index fc542acf1b3da..1be41b88a1022 100644 --- a/packages/@aws-cdk/aws-neptune/README.md +++ b/packages/@aws-cdk/aws-neptune/README.md @@ -58,6 +58,24 @@ attributes: const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" ``` +## IAM Authentication + +You can also authenticate to a database cluster using AWS Identity and Access Management (IAM) database authentication; +See for more information and a list of supported +versions and limitations. + +The following example shows enabling IAM authentication for a database cluster and granting connection access to an IAM role. + +```ts +const cluster = new rds.DatabaseCluster(stack, 'Cluster', { + vpc, + instanceType: neptune.InstanceType.R5_LARGE, + iamAuthentication: true, // Optional - will be automatically set if you call grantConnect(). +}); +const role = new Role(stack, 'DBRole', { assumedBy: new AccountPrincipal(stack.account) }); +instance.grantConnect(role); // Grant the role connection access to the DB. +``` + ## Customizing parameters Neptune allows configuring database behavior by supplying custom parameter groups. For more details, refer to the diff --git a/packages/@aws-cdk/aws-neptune/lib/cluster.ts b/packages/@aws-cdk/aws-neptune/lib/cluster.ts index 4adbe2ce8ea04..316795c23a491 100644 --- a/packages/@aws-cdk/aws-neptune/lib/cluster.ts +++ b/packages/@aws-cdk/aws-neptune/lib/cluster.ts @@ -1,7 +1,7 @@ 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 { Duration, IResource, RemovalPolicy, Resource, Token } from '@aws-cdk/core'; +import { Aws, Duration, IResource, Lazy, RemovalPolicy, Resource, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Endpoint } from './endpoint'; import { InstanceType } from './instance'; @@ -119,6 +119,13 @@ export interface DatabaseClusterProps { */ readonly dbClusterName?: string; + /** + * Map AWS Identity and Access Management (IAM) accounts to database accounts + * + * @default - `false` + */ + readonly iamAuthentication?: boolean; + /** * Base identifier for instances * @@ -233,6 +240,11 @@ export interface IDatabaseCluster extends IResource, ec2.IConnectable { * @attribute ReadEndpoint */ readonly clusterReadEndpoint: Endpoint; + + /** + * Grant the given identity connection access to the database. + */ + grantConnect(grantee: iam.IGrantable): iam.Grant; } /** @@ -266,23 +278,15 @@ export interface DatabaseClusterAttributes { } /** - * Create a clustered database with a given number of instances. - * - * @resource AWS::Neptune::DBCluster + * A new or imported database cluster. */ -export class DatabaseCluster extends Resource implements IDatabaseCluster { - - /** - * The default number of instances in the Neptune cluster if none are - * specified - */ - public static readonly DEFAULT_NUM_INSTANCES = 1; +export abstract class DatabaseClusterBase extends Resource implements IDatabaseCluster { /** * Import an existing DatabaseCluster from properties */ public static fromDatabaseClusterAttributes(scope: Construct, id: string, attrs: DatabaseClusterAttributes): IDatabaseCluster { - class Import extends Resource implements IDatabaseCluster { + class Import extends DatabaseClusterBase implements IDatabaseCluster { public readonly defaultPort = ec2.Port.tcp(attrs.port); public readonly connections = new ec2.Connections({ securityGroups: [attrs.securityGroup], @@ -291,6 +295,7 @@ export class DatabaseCluster extends Resource implements IDatabaseCluster { public readonly clusterIdentifier = attrs.clusterIdentifier; public readonly clusterEndpoint = new Endpoint(attrs.clusterEndpointAddress, attrs.port); public readonly clusterReadEndpoint = new Endpoint(attrs.readerEndpointAddress, attrs.port); + protected enableIamAuthentication = true; } return new Import(scope, id); @@ -299,17 +304,65 @@ export class DatabaseCluster extends Resource implements IDatabaseCluster { /** * Identifier of the cluster */ - public readonly clusterIdentifier: string; + public abstract readonly clusterIdentifier: string; /** * The endpoint to use for read/write operations */ - public readonly clusterEndpoint: Endpoint; + public abstract readonly clusterEndpoint: Endpoint; /** * Endpoint to use for load-balanced read-only operations. */ + public abstract readonly clusterReadEndpoint: Endpoint; + + /** + * The connections object to implement IConnectable + */ + public abstract readonly connections: ec2.Connections; + + protected abstract enableIamAuthentication?: boolean; + + public grantConnect(grantee: iam.IGrantable): iam.Grant { + if (this.enableIamAuthentication === false) { + throw new Error('Cannot grant connect when IAM authentication is disabled'); + } + + this.enableIamAuthentication = true; + return iam.Grant.addToPrincipal({ + grantee, + actions: ['neptune-db:*'], + resourceArns: [ + [ + 'arn', + Aws.PARTITION, + 'neptune-db', + Aws.REGION, + Aws.ACCOUNT_ID, + `${this.clusterIdentifier}/*`, + ].join(':'), + ], + }); + } +} + +/** + * Create a clustered database with a given number of instances. + * + * @resource AWS::Neptune::DBCluster + */ +export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseCluster { + + /** + * The default number of instances in the Neptune cluster if none are + * specified + */ + public static readonly DEFAULT_NUM_INSTANCES = 1; + + public readonly clusterIdentifier: string; + public readonly clusterEndpoint: Endpoint; public readonly clusterReadEndpoint: Endpoint; + public readonly connections: ec2.Connections; /** * The resource id for the cluster; for example: cluster-ABCD1234EFGH5678IJKL90MNOP. The cluster ID uniquely @@ -318,11 +371,6 @@ export class DatabaseCluster extends Resource implements IDatabaseCluster { */ public readonly clusterResourceIdentifier: string; - /** - * The connections object to implement IConectable - */ - public readonly connections: ec2.Connections; - /** * The VPC where the DB subnet group is created. */ @@ -348,6 +396,8 @@ export class DatabaseCluster extends Resource implements IDatabaseCluster { */ public readonly instanceEndpoints: Endpoint[] = []; + protected enableIamAuthentication?: boolean; + constructor(scope: Construct, id: string, props: DatabaseClusterProps) { super(scope, id); @@ -385,6 +435,8 @@ export class DatabaseCluster extends Resource implements IDatabaseCluster { const deletionProtection = props.deletionProtection ?? (props.removalPolicy === RemovalPolicy.RETAIN ? true : undefined); + this.enableIamAuthentication = props.iamAuthentication; + // Create the Neptune cluster const cluster = new CfnDBCluster(this, 'Resource', { // Basic @@ -396,6 +448,7 @@ export class DatabaseCluster extends Resource implements IDatabaseCluster { dbClusterParameterGroupName: props.clusterParameterGroup?.clusterParameterGroupName, deletionProtection: deletionProtection, associatedRoles: props.associatedRoles ? props.associatedRoles.map(role => ({ roleArn: role.roleArn })) : undefined, + iamAuthEnabled: Lazy.any({ produce: () => this.enableIamAuthentication }), // Backup backupRetentionPeriod: props.backupRetention?.toDays(), preferredBackupWindow: props.preferredBackupWindow, diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts index d2c5ff4b6c1ef..6bb933e34dab5 100644 --- a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts @@ -1,4 +1,5 @@ -import { expect as expectCDK, haveResource, ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { ABSENT, ResourcePart } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; @@ -20,7 +21,7 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { Properties: { DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], @@ -28,20 +29,20 @@ describe('DatabaseCluster', () => { }, DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + expect(stack).toHaveResource('AWS::Neptune::DBInstance', { DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - expectCDK(stack).to(haveResource('AWS::Neptune::DBSubnetGroup', { + expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { SubnetIds: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, { Ref: 'VPCPrivateSubnet3Subnet3EDCD457' }, ], - })); + }); }); test('can create a cluster with a single instance', () => { @@ -57,10 +58,10 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], - })); + }); }); test('errors when less than one instance is specified', () => { @@ -111,11 +112,11 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { EngineVersion: '1.0.4.1', DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], - })); + }); }); test('can create a cluster with imported vpc and security group', () => { @@ -135,10 +136,10 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, VpcSecurityGroupIds: ['SecurityGroupId12345'], - })); + }); }); test('cluster with parameter group', () => { @@ -160,9 +161,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { DBClusterParameterGroupName: { Ref: 'ParamsA8366201' }, - })); + }); }); test('cluster with associated role', () => { @@ -183,7 +184,7 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { AssociatedRoles: [ { RoleArn: { @@ -194,7 +195,7 @@ describe('DatabaseCluster', () => { }, }, ], - })); + }); }); test('cluster with imported parameter group', () => { @@ -212,9 +213,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { DBClusterParameterGroupName: 'ParamGroupName', - })); + }); }); test('create an encrypted cluster with custom KMS key', () => { @@ -230,7 +231,7 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { KmsKeyId: { 'Fn::GetAtt': [ 'Key961B73FD', @@ -238,7 +239,7 @@ describe('DatabaseCluster', () => { ], }, StorageEncrypted: true, - })); + }); }); test('creating a cluster defaults to using encryption', () => { @@ -253,9 +254,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { StorageEncrypted: true, - })); + }); }); test('supplying a KMS key with storageEncryption false throws an error', () => { @@ -306,9 +307,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + expect(stack).toHaveResource('AWS::Neptune::DBInstance', { DBInstanceIdentifier: `${instanceIdentifierBase}1`, - })); + }); }); test('cluster identifier used', () => { @@ -325,9 +326,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + expect(stack).toHaveResource('AWS::Neptune::DBInstance', { DBInstanceIdentifier: `${clusterIdentifier}instance1`, - })); + }); }); test('imported cluster has supplied attributes', () => { @@ -370,9 +371,9 @@ describe('DatabaseCluster', () => { cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expectCDK(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', - })); + }); }); test('backup retention period respected', () => { @@ -388,9 +389,9 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { BackupRetentionPeriod: 20, - })); + }); }); test('backup maintenance window respected', () => { @@ -407,10 +408,10 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { BackupRetentionPeriod: 20, PreferredBackupWindow: '07:34-08:04', - })); + }); }); test('regular maintenance window respected', () => { @@ -426,9 +427,78 @@ describe('DatabaseCluster', () => { }); // THEN - expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + expect(stack).toHaveResource('AWS::Neptune::DBCluster', { PreferredMaintenanceWindow: '07:34-08:04', - })); + }); + }); + + test('iam authentication - off by default', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Cluster', { + vpc, + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Neptune::DBCluster', { + IamAuthEnabled: ABSENT, + }); + }); + + test('createGrant - creates IAM policy and enables IAM auth', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const cluster = new DatabaseCluster(stack, 'Cluster', { + vpc, + instanceType: InstanceType.R5_LARGE, + }); + const role = new iam.Role(stack, 'DBRole', { + assumedBy: new iam.AccountPrincipal(stack.account), + }); + cluster.grantConnect(role); + + // THEN + expect(stack).toHaveResourceLike('AWS::Neptune::DBCluster', { + IamAuthEnabled: true, + }); + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Effect: 'Allow', + Action: 'neptune-db:*', + Resource: { + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':neptune-db:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'ClusterEB0386A7' }, '/*']], + }, + }], + Version: '2012-10-17', + }, + }); + }); + + test('createGrant - throws if IAM auth disabled', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const cluster = new DatabaseCluster(stack, 'Cluster', { + vpc, + instanceType: InstanceType.R5_LARGE, + iamAuthentication: false, + }); + const role = new iam.Role(stack, 'DBRole', { + assumedBy: new iam.AccountPrincipal(stack.account), + }); + + // THEN + expect(() => { cluster.grantConnect(role); }).toThrow(/Cannot grant connect when IAM authentication is disabled/); }); }); diff --git a/packages/@aws-cdk/aws-route53-targets/README.md b/packages/@aws-cdk/aws-route53-targets/README.md index 5270482564ee8..ba9f1ea7e4831 100644 --- a/packages/@aws-cdk/aws-route53-targets/README.md +++ b/packages/@aws-cdk/aws-route53-targets/README.md @@ -63,6 +63,19 @@ This library contains Route53 Alias Record targets for: For example, if the Amazon-provided DNS for the load balancer is `ALB-xxxxxxx.us-west-2.elb.amazonaws.com`, CDK will create alias target in Route 53 will be `dualstack.ALB-xxxxxxx.us-west-2.elb.amazonaws.com`. +* GlobalAccelerator + + ```ts + new route53.ARecord(stack, 'AliasRecord', { + zone, + target: route53.RecordTarget.fromAlias(new targets.GlobalAcceleratorTarget(accelerator)), + // or - route53.RecordTarget.fromAlias(new targets.GlobalAcceleratorDomainTarget('xyz.awsglobalaccelerator.com')), + }); + ``` + +**Important:** If you use GlobalAcceleratorDomainTarget, passing a string rather than an instance of IAccelerator, ensure that the string is a valid domain name of an existing Global Accelerator instance. +See [the documentation on DNS addressing](https://docs.aws.amazon.com/global-accelerator/latest/dg/dns-addressing-custom-domains.dns-addressing.html) with Global Accelerator for more info. + * InterfaceVpcEndpoints **Important:** Based on the CFN docs for VPCEndpoints - [see here](attrDnsEntries) - the attributes returned for DnsEntries in CloudFormation is a combination of the hosted zone ID and the DNS name. The entries are ordered as follows: regional public DNS, zonal public DNS, private DNS, and wildcard DNS. This order is not enforced for AWS Marketplace services, and therefore this CDK construct is ONLY guaranteed to work with non-marketplace services. diff --git a/packages/@aws-cdk/aws-route53-targets/lib/global-accelerator-target.ts b/packages/@aws-cdk/aws-route53-targets/lib/global-accelerator-target.ts new file mode 100644 index 0000000000000..d80aaaa140b1b --- /dev/null +++ b/packages/@aws-cdk/aws-route53-targets/lib/global-accelerator-target.ts @@ -0,0 +1,41 @@ +import * as globalaccelerator from '@aws-cdk/aws-globalaccelerator'; +import * as route53 from '@aws-cdk/aws-route53'; + + +/** + * Use a Global Accelerator domain name as an alias record target. + */ +export class GlobalAcceleratorDomainTarget implements route53.IAliasRecordTarget { + /** + * The hosted zone Id if using an alias record in Route53. + * This value never changes. + * Ref: https://docs.aws.amazon.com/general/latest/gr/global_accelerator.html + */ + public static readonly GLOBAL_ACCELERATOR_ZONE_ID = 'Z2BJ6XQ5FK7U4H'; + + /** + * Create an Alias Target for a Global Accelerator domain name. + */ + constructor(private readonly acceleratorDomainName: string) { + } + + bind(_record: route53.IRecordSet): route53.AliasRecordTargetConfig { + return { + hostedZoneId: GlobalAcceleratorTarget.GLOBAL_ACCELERATOR_ZONE_ID, + dnsName: this.acceleratorDomainName, + }; + } +} + +/** + * Use a Global Accelerator instance domain name as an alias record target. + */ +export class GlobalAcceleratorTarget extends GlobalAcceleratorDomainTarget { + + /** + * Create an Alias Target for a Global Accelerator instance. + */ + constructor(accelerator: globalaccelerator.IAccelerator) { + super(accelerator.dnsName); + } +} diff --git a/packages/@aws-cdk/aws-route53-targets/lib/index.ts b/packages/@aws-cdk/aws-route53-targets/lib/index.ts index af574aa599519..5c8b86fb959c1 100644 --- a/packages/@aws-cdk/aws-route53-targets/lib/index.ts +++ b/packages/@aws-cdk/aws-route53-targets/lib/index.ts @@ -6,3 +6,4 @@ export * from './cloudfront-target'; export * from './load-balancer-target'; export * from './interface-vpc-endpoint-target'; export * from './userpool-domain'; +export * from './global-accelerator-target'; diff --git a/packages/@aws-cdk/aws-route53-targets/package.json b/packages/@aws-cdk/aws-route53-targets/package.json index 08ea2cb9e91f4..d98999baaab6a 100644 --- a/packages/@aws-cdk/aws-route53-targets/package.json +++ b/packages/@aws-cdk/aws-route53-targets/package.json @@ -80,6 +80,7 @@ "@aws-cdk/aws-elasticloadbalancing": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-globalaccelerator": "0.0.0", "@aws-cdk/aws-route53": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", @@ -96,6 +97,7 @@ "@aws-cdk/aws-elasticloadbalancing": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-globalaccelerator": "0.0.0", "@aws-cdk/aws-route53": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53-targets/test/global-accelerator-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/global-accelerator-target.test.ts new file mode 100644 index 0000000000000..07db27b92940e --- /dev/null +++ b/packages/@aws-cdk/aws-route53-targets/test/global-accelerator-target.test.ts @@ -0,0 +1,59 @@ +import '@aws-cdk/assert/jest'; +import * as globalaccelerator from '@aws-cdk/aws-globalaccelerator'; +import * as route53 from '@aws-cdk/aws-route53'; +import { Stack } from '@aws-cdk/core'; +import * as targets from '../lib'; + +test('GlobalAcceleratorTarget exposes a public constant of the zone id', () => { + expect(targets.GlobalAcceleratorTarget.GLOBAL_ACCELERATOR_ZONE_ID).toStrictEqual('Z2BJ6XQ5FK7U4H'); + expect(targets.GlobalAcceleratorDomainTarget.GLOBAL_ACCELERATOR_ZONE_ID).toStrictEqual('Z2BJ6XQ5FK7U4H'); +}); + +test('GlobalAcceleratorTarget creates an alias resource with a string domain name', () => { + // GIVEN + const stack = new Stack(); + const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' }); + + // WHEN + new route53.ARecord(stack, 'GlobalAcceleratorAlias', { + target: route53.RecordTarget.fromAlias(new targets.GlobalAcceleratorDomainTarget('xyz.awsglobalaccelerator.com')), + recordName: 'test', + zone, + }); + + // THEN + expect(stack).toHaveResource('AWS::Route53::RecordSet', { + AliasTarget: { + DNSName: 'xyz.awsglobalaccelerator.com', + HostedZoneId: 'Z2BJ6XQ5FK7U4H', + }, + }); +}); + +test('GlobalAcceleratorTarget creates an alias resource with a Global Accelerator reference domain name', () => { + // GIVEN + const stack = new Stack(); + const accelerator = new globalaccelerator.Accelerator(stack, 'Accelerator'); + const logicalId = stack.getLogicalId(accelerator.node.defaultChild); + const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' }); + + // WHEN + new route53.ARecord(stack, 'GlobalAcceleratorAlias', { + target: route53.RecordTarget.fromAlias(new targets.GlobalAcceleratorTarget(accelerator)), + recordName: 'test', + zone, + }); + + // THEN + expect(stack).toHaveResource('AWS::Route53::RecordSet', { + AliasTarget: { + DNSName: { + 'Fn::GetAtt': [ + logicalId, + 'DnsName', + ], + }, + HostedZoneId: 'Z2BJ6XQ5FK7U4H', + }, + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53-targets/test/integ.globalaccelerator-alias-target.expected.json b/packages/@aws-cdk/aws-route53-targets/test/integ.globalaccelerator-alias-target.expected.json new file mode 100644 index 0000000000000..e11d21275bbab --- /dev/null +++ b/packages/@aws-cdk/aws-route53-targets/test/integ.globalaccelerator-alias-target.expected.json @@ -0,0 +1,52 @@ +{ + "Resources": { + "Accelerator8EB0B6B1": { + "Type": "AWS::GlobalAccelerator::Accelerator", + "Properties": { + "Name": "aws-cdk-globalaccelerator-integ", + "Enabled": true + } + }, + "HostedZoneDB99F866": { + "Type": "AWS::Route53::HostedZone", + "Properties": { + "Name": "test.public." + } + }, + "LocalGlobalAcceleratorAlias18B4A87A": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "Name": "test-local.test.public.", + "Type": "A", + "AliasTarget": { + "DNSName": { + "Fn::GetAtt": [ + "Accelerator8EB0B6B1", + "DnsName" + ] + }, + "HostedZoneId": "Z2BJ6XQ5FK7U4H" + }, + "Comment": "Alias to the locally created Global Accelerator", + "HostedZoneId": { + "Ref": "HostedZoneDB99F866" + } + } + }, + "ExistingGlobalAcceleratorAlias7ACF888C": { + "Type": "AWS::Route53::RecordSet", + "Properties": { + "Name": "test-existing.test.public.", + "Type": "A", + "AliasTarget": { + "DNSName": "someexisting.awsglobalaccelerator.com", + "HostedZoneId": "Z2BJ6XQ5FK7U4H" + }, + "Comment": "Alias to the an existing Global Accelerator", + "HostedZoneId": { + "Ref": "HostedZoneDB99F866" + } + } + } + } + } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53-targets/test/integ.globalaccelerator-alias-target.ts b/packages/@aws-cdk/aws-route53-targets/test/integ.globalaccelerator-alias-target.ts new file mode 100644 index 0000000000000..560e828accaa5 --- /dev/null +++ b/packages/@aws-cdk/aws-route53-targets/test/integ.globalaccelerator-alias-target.ts @@ -0,0 +1,31 @@ +#!/usr/bin/env node +import * as globalaccelerator from '@aws-cdk/aws-globalaccelerator'; +import * as route53 from '@aws-cdk/aws-route53'; +import * as cdk from '@aws-cdk/core'; +import * as targets from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-globalaccelerator-integ'); + +let accelerator = new globalaccelerator.Accelerator(stack, 'Accelerator', { + acceleratorName: `${stack.stackName}`, + enabled: true, +}); + +const zone = new route53.PublicHostedZone(stack, 'HostedZone', { zoneName: 'test.public' }); + +new route53.ARecord(stack, 'LocalGlobalAcceleratorAlias', { + comment: 'Alias to the locally created Global Accelerator', + target: route53.RecordTarget.fromAlias(new targets.GlobalAcceleratorTarget(accelerator)), + recordName: 'test-local', + zone, +}); + +new route53.ARecord(stack, 'ExistingGlobalAcceleratorAlias', { + comment: 'Alias to the an existing Global Accelerator', + target: route53.RecordTarget.fromAlias(new targets.GlobalAcceleratorDomainTarget('someexisting.awsglobalaccelerator.com')), + recordName: 'test-existing', + zone, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index d6a45e77cb7e1..6a83cacd4bb92 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -43,7 +43,7 @@ new Bucket(this, 'MyFirstBucket'); Define a KMS-encrypted bucket: ```ts -const bucket = new Bucket(this, 'MyUnencryptedBucket', { +const bucket = new Bucket(this, 'MyEncryptedBucket', { encryption: BucketEncryption.KMS }); diff --git a/packages/@aws-cdk/aws-sam/package.json b/packages/@aws-cdk/aws-sam/package.json index 6643799114182..686ceb3b92b41 100644 --- a/packages/@aws-cdk/aws-sam/package.json +++ b/packages/@aws-cdk/aws-sam/package.json @@ -78,7 +78,7 @@ "cfn2ts": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.5.2" + "ts-jest": "^26.5.3" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index d7c2d1498394d..69d97936aabcb 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -102,8 +102,8 @@ The following example provides the field named `input` as the input to the `Task state that runs a Lambda function. ```ts -const submitJob = new tasks.LambdaInvoke(stack, 'Invoke Handler', { - lambdaFunction: submitJobLambda, +const submitJob = new tasks.LambdaInvoke(this, 'Invoke Handler', { + lambdaFunction: fn, inputPath: '$.input' }); ``` @@ -122,8 +122,8 @@ as well as other metadata. The following example assigns the output from the Task to a field named `result` ```ts -const submitJob = new tasks.LambdaInvoke(stack, 'Invoke Handler', { - lambdaFunction: submitJobLambda, +const submitJob = new tasks.LambdaInvoke(this, 'Invoke Handler', { + lambdaFunction: fn, outputPath: '$.Payload.result' }); ``` @@ -140,9 +140,11 @@ The following example adds the item from calling DynamoDB's `getItem` API to the input and passes it to the next state. ```ts -new tasks.DynamoGetItem(this, 'PutItem', { - item: { MessageId: { s: '12345'} }, - tableName: 'my-table', +new tasks.DynamoPutItem(this, 'PutItem', { + item: { + MessageId: tasks.DynamoAttributeValue.fromString('message-id') + }, + table: myTable, resultPath: `$.Item`, }); ``` @@ -163,10 +165,10 @@ The following example provides the field named `input` as the input to the Lambd and invokes it asynchronously. ```ts -const submitJob = new tasks.LambdaInvoke(stack, 'Invoke Handler', { - lambdaFunction: submitJobLambda, - payload: sfn.JsonPath.StringAt('$.input'), - invocationType: tasks.InvocationType.EVENT, +const submitJob = new tasks.LambdaInvoke(this, 'Invoke Handler', { + lambdaFunction: fn, + payload: sfn.TaskInput.fromDataAt('$.input'), + invocationType: tasks.LambdaInvocationType.EVENT, }); ``` @@ -194,7 +196,7 @@ const createMessage = new tasks.EvaluateExpression(this, 'Create message', { }); const publishMessage = new tasks.SnsPublish(this, 'Publish message', { - topic, + topic: new sns.Topic(this, 'cool-topic'), message: sfn.TaskInput.fromDataAt('$.message'), resultPath: '$.sns', }); @@ -224,19 +226,19 @@ Step Functions supports [Athena](https://docs.aws.amazon.com/step-functions/late The [StartQueryExecution](https://docs.aws.amazon.com/athena/latest/APIReference/API_StartQueryExecution.html) API runs the SQL query statement. ```ts -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from `@aws-cdk/aws-stepfunctions-tasks`; - -const startQueryExecutionJob = new tasks.AthenaStartQueryExecution(stack, 'Start Athena Query', { +const startQueryExecutionJob = new tasks.AthenaStartQueryExecution(this, 'Start Athena Query', { queryString: sfn.JsonPath.stringAt('$.queryString'), queryExecutionContext: { - database: 'mydatabase', + databaseName: 'mydatabase', }, resultConfiguration: { encryptionConfiguration: { encryptionOption: tasks.EncryptionOption.S3_MANAGED, }, - outputLocation: sfn.JsonPath.stringAt('$.outputLocation'), + outputLocation: { + bucketName: 'query-results-bucket', + objectKey: 'folder', + }, }, }); ``` @@ -246,10 +248,7 @@ const startQueryExecutionJob = new tasks.AthenaStartQueryExecution(stack, 'Start The [GetQueryExecution](https://docs.aws.amazon.com/athena/latest/APIReference/API_GetQueryExecution.html) API gets information about a single execution of a query. ```ts -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from `@aws-cdk/aws-stepfunctions-tasks`; - -const getQueryExecutionJob = new tasks.AthenaGetQueryExecution(stack, 'Get Query Execution', { +const getQueryExecutionJob = new tasks.AthenaGetQueryExecution(this, 'Get Query Execution', { queryExecutionId: sfn.JsonPath.stringAt('$.QueryExecutionId'), }); ``` @@ -259,10 +258,7 @@ const getQueryExecutionJob = new tasks.AthenaGetQueryExecution(stack, 'Get Query The [GetQueryResults](https://docs.aws.amazon.com/athena/latest/APIReference/API_GetQueryResults.html) API that streams the results of a single query execution specified by QueryExecutionId from S3. ```ts -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from `@aws-cdk/aws-stepfunctions-tasks`; - -const getQueryResultsJob = new tasks.AthenaGetQueryResults(stack, 'Get Query Results', { +const getQueryResultsJob = new tasks.AthenaGetQueryResults(this, 'Get Query Results', { queryExecutionId: sfn.JsonPath.stringAt('$.QueryExecutionId'), }); ``` @@ -272,10 +268,7 @@ const getQueryResultsJob = new tasks.AthenaGetQueryResults(stack, 'Get Query Res The [StopQueryExecution](https://docs.aws.amazon.com/athena/latest/APIReference/API_StopQueryExecution.html) API that stops a query execution. ```ts -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from `@aws-cdk/aws-stepfunctions-tasks`; - -const stopQueryExecutionJob = new tasks.AthenaStopQueryExecution(stack, 'Stop Query Execution', { +const stopQueryExecutionJob = new tasks.AthenaStopQueryExecution(this, 'Stop Query Execution', { queryExecutionId: sfn.JsonPath.stringAt('$.QueryExecutionId'), }); ``` @@ -288,27 +281,7 @@ Step Functions supports [Batch](https://docs.aws.amazon.com/step-functions/lates The [SubmitJob](https://docs.aws.amazon.com/batch/latest/APIReference/API_SubmitJob.html) API submits an AWS Batch job from a job definition. -```ts -import * as batch from '@aws-cdk/aws-batch'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; - -const batchQueue = new batch.JobQueue(this, 'JobQueue', { - computeEnvironments: [ - { - order: 1, - computeEnvironment: new batch.ComputeEnvironment(this, 'ComputeEnv', { - computeResources: { vpc }, - }), - }, - ], -}); - -const batchJobDefinition = new batch.JobDefinition(this, 'JobDefinition', { - container: { - image: ecs.ContainerImage.fromAsset(path.resolve(__dirname, 'batchjob-image')), - }, -}); - +```ts fixture=with-batch-job const task = new tasks.BatchSubmitJob(this, 'Submit Job', { jobDefinition: batchJobDefinition, jobName: 'MyJob', @@ -326,10 +299,8 @@ Step Functions supports [CodeBuild](https://docs.aws.amazon.com/step-functions/l ```ts import * as codebuild from '@aws-cdk/aws-codebuild'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; -const codebuildProject = new codebuild.Project(stack, 'Project', { +const codebuildProject = new codebuild.Project(this, 'Project', { projectName: 'MyTestProject', buildSpec: codebuild.BuildSpec.fromObject({ version: '0.2', @@ -343,7 +314,7 @@ const codebuildProject = new codebuild.Project(stack, 'Project', { }), }); -const task = new tasks.CodeBuildStartBuild(stack, 'Task', { +const task = new tasks.CodeBuildStartBuild(this, 'Task', { project: codebuildProject, integrationPattern: sfn.IntegrationPattern.RUN_JOB, environmentVariablesOverride: { @@ -367,7 +338,7 @@ The [GetItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API ```ts new tasks.DynamoGetItem(this, 'Get Item', { key: { messageId: tasks.DynamoAttributeValue.fromString('message-007') }, - table, + table: myTable, }); ``` @@ -382,7 +353,7 @@ new tasks.DynamoPutItem(this, 'PutItem', { Text: tasks.DynamoAttributeValue.fromString(sfn.JsonPath.stringAt('$.bar')), TotalCount: tasks.DynamoAttributeValue.fromNumber(10), }, - table, + table: myTable, }); ``` @@ -391,12 +362,9 @@ new tasks.DynamoPutItem(this, 'PutItem', { The [DeleteItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html) operation deletes a single item in a table by primary key. ```ts -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; - new tasks.DynamoDeleteItem(this, 'DeleteItem', { key: { MessageId: tasks.DynamoAttributeValue.fromString('message-007') }, - table, + table: myTable, resultPath: sfn.JsonPath.DISCARD, }); ``` @@ -408,8 +376,10 @@ to the table if it does not already exist. ```ts new tasks.DynamoUpdateItem(this, 'UpdateItem', { - key: { MessageId: tasks.DynamoAttributeValue.fromString('message-007') }, - table, + key: { + MessageId: tasks.DynamoAttributeValue.fromString('message-007') + }, + table: myTable, expressionAttributeValues: { ':val': tasks.DynamoAttributeValue.numberFromString(sfn.JsonPath.stringAt('$.Item.TotalCount.N')), ':rand': tasks.DynamoAttributeValue.fromNumber(20), @@ -443,20 +413,18 @@ The following example runs a job from a task definition on EC2 ```ts import * as ecs from '@aws-cdk/aws-ecs'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; -const vpc = ec2.Vpc.fromLookup(stack, 'Vpc', { +const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { isDefault: true, }); -const cluster = new ecs.Cluster(stack, 'Ec2Cluster', { vpc }); +const cluster = new ecs.Cluster(this, 'Ec2Cluster', { vpc }); cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro'), vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, }); -const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { +const taskDefinition = new ecs.TaskDefinition(this, 'TD', { compatibility: ecs.Compatibility.EC2, }); @@ -465,7 +433,7 @@ taskDefinition.addContainer('TheContainer', { memoryLimitMiB: 256, }); -const runTask = new tasks.EcsRunTask(stack, 'Run', { +const runTask = new tasks.EcsRunTask(this, 'Run', { integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, @@ -500,16 +468,14 @@ The following example runs a job from a task definition on Fargate ```ts import * as ecs from '@aws-cdk/aws-ecs'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; -const vpc = ec2.Vpc.fromLookup(stack, 'Vpc', { +const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { isDefault: true, }); -const cluster = new ecs.Cluster(stack, 'FargateCluster', { vpc }); +const cluster = new ecs.Cluster(this, 'FargateCluster', { vpc }); -const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { +const taskDefinition = new ecs.TaskDefinition(this, 'TD', { memoryMiB: '512', cpu: '256', compatibility: ecs.Compatibility.FARGATE, @@ -520,7 +486,7 @@ const containerDefinition = taskDefinition.addContainer('TheContainer', { memoryLimitMiB: 256, }); -const runTask = new tasks.EcsRunTask(stack, 'RunFargate', { +const runTask = new tasks.EcsRunTask(this, 'RunFargate', { integrationPattern: sfn.IntegrationPattern.RUN_JOB, cluster, taskDefinition, @@ -548,15 +514,15 @@ Corresponds to the [`runJobFlow`](https://docs.aws.amazon.com/emr/latest/APIRefe ```ts -const clusterRole = new iam.Role(stack, 'ClusterRole', { +const clusterRole = new iam.Role(this, 'ClusterRole', { assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'), }); -const serviceRole = new iam.Role(stack, 'ServiceRole', { +const serviceRole = new iam.Role(this, 'ServiceRole', { assumedBy: new iam.ServicePrincipal('elasticmapreduce.amazonaws.com'), }); -const autoScalingRole = new iam.Role(stack, 'AutoScalingRole', { +const autoScalingRole = new iam.Role(this, 'AutoScalingRole', { assumedBy: new iam.ServicePrincipal('elasticmapreduce.amazonaws.com'), }); @@ -569,16 +535,15 @@ autoScalingRole.assumeRolePolicy?.addStatements( actions: [ 'sts:AssumeRole', ], - }); + })); ) -new tasks.EmrCreateCluster(stack, 'Create Cluster', { +new tasks.EmrCreateCluster(this, 'Create Cluster', { instances: {}, clusterRole, name: sfn.TaskInput.fromDataAt('$.ClusterName').value, serviceRole, autoScalingRole, - integrationPattern: sfn.ServiceIntegrationPattern.FIRE_AND_FORGET, }); ``` @@ -590,7 +555,7 @@ terminated by user intervention, an API call, or a job-flow error. Corresponds to the [`setTerminationProtection`](https://docs.aws.amazon.com/step-functions/latest/dg/connect-emr.html) API in EMR. ```ts -new tasks.EmrSetClusterTerminationProtection(stack, 'Task', { +new tasks.EmrSetClusterTerminationProtection(this, 'Task', { clusterId: 'ClusterId', terminationProtected: false, }); @@ -602,7 +567,7 @@ Shuts down a cluster (job flow). Corresponds to the [`terminateJobFlows`](https://docs.aws.amazon.com/emr/latest/APIReference/API_TerminateJobFlows.html) API in EMR. ```ts -new tasks.EmrTerminateCluster(stack, 'Task', { +new tasks.EmrTerminateCluster(this, 'Task', { clusterId: 'ClusterId' }); ``` @@ -613,7 +578,7 @@ Adds a new step to a running cluster. Corresponds to the [`addJobFlowSteps`](https://docs.aws.amazon.com/emr/latest/APIReference/API_AddJobFlowSteps.html) API in EMR. ```ts -new tasks.EmrAddStep(stack, 'Task', { +new tasks.EmrAddStep(this, 'Task', { clusterId: 'ClusterId', name: 'StepName', jar: 'Jar', @@ -627,7 +592,7 @@ Cancels a pending step in a running cluster. Corresponds to the [`cancelSteps`](https://docs.aws.amazon.com/emr/latest/APIReference/API_CancelSteps.html) API in EMR. ```ts -new tasks.EmrCancelStep(stack, 'Task', { +new tasks.EmrCancelStep(this, 'Task', { clusterId: 'ClusterId', stepId: 'StepId', }); @@ -641,7 +606,7 @@ fleet with the specified InstanceFleetName. Corresponds to the [`modifyInstanceFleet`](https://docs.aws.amazon.com/emr/latest/APIReference/API_ModifyInstanceFleet.html) API in EMR. ```ts -new sfn.EmrModifyInstanceFleetByName(stack, 'Task', { +new tasks.EmrModifyInstanceFleetByName(this, 'Task', { clusterId: 'ClusterId', instanceFleetName: 'InstanceFleetName', targetOnDemandCapacity: 2, @@ -656,7 +621,7 @@ Modifies the number of nodes and configuration settings of an instance group. Corresponds to the [`modifyInstanceGroups`](https://docs.aws.amazon.com/emr/latest/APIReference/API_ModifyInstanceGroups.html) API in EMR. ```ts -new tasks.EmrModifyInstanceGroupByName(stack, 'Task', { +new tasks.EmrModifyInstanceGroupByName(this, 'Task', { clusterId: 'ClusterId', instanceGroupName: sfn.JsonPath.stringAt('$.InstanceGroupName'), instanceGroup: { @@ -703,11 +668,11 @@ Step Functions supports [AWS Glue](https://docs.aws.amazon.com/step-functions/la You can call the [`StartJobRun`](https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-runs.html#aws-glue-api-jobs-runs-StartJobRun) API from a `Task` state. ```ts -new GlueStartJobRun(stack, 'Task', { +new tasks.GlueStartJobRun(this, 'Task', { glueJobName: 'my-glue-job', - arguments: { - key: 'value', - }, + arguments: sfn.TaskInput.fromObject({ + key: 'value', + }), timeout: cdk.Duration.minutes(30), notifyDelayAfter: cdk.Duration.minutes(5), }); @@ -720,8 +685,8 @@ Step Functions supports [AWS Glue DataBrew](https://docs.aws.amazon.com/step-fun 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', +new tasks.GlueDataBrewStartJobRun(this, 'Task', { + name: 'databrew-job', }); ``` @@ -737,23 +702,8 @@ The following snippet invokes a Lambda Function with the state input as the payl by referencing the `$` path. ```ts -import * as lambda from '@aws-cdk/aws-lambda'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; - -const myLambda = new lambda.Function(this, 'my sample lambda', { - code: Code.fromInline(`exports.handler = async () => { - return { - statusCode: '200', - body: 'hello, world!' - }; - };`), - runtime: Runtime.NODEJS_12_X, - handler: 'index.handler', -}); - new tasks.LambdaInvoke(this, 'Invoke with state input', { - lambdaFunction: myLambda, + lambdaFunction: fn, }); ``` @@ -768,13 +718,13 @@ to reference the output of a Lambda executed before it. ```ts new tasks.LambdaInvoke(this, 'Invoke with empty object as payload', { - lambdaFunction: myLambda, + lambdaFunction: fn, payload: sfn.TaskInput.fromObject({}), }); -// use the output of myLambda as input +// use the output of fn as input new tasks.LambdaInvoke(this, 'Invoke with payload field in the state input', { - lambdaFunction: myOtherLambda, + lambdaFunction: fn, payload: sfn.TaskInput.fromDataAt('$.Payload'), }); ``` @@ -784,8 +734,7 @@ the Lambda function response. ```ts new tasks.LambdaInvoke(this, 'Invoke and set function response as task output', { - lambdaFunction: myLambda, - payload: sfn.TaskInput.fromDataAt('$'), + lambdaFunction: fn, outputPath: '$.Payload', }); ``` @@ -797,9 +746,9 @@ integrationPattern, invocationType, clientContext, and qualifier properties. ```ts new tasks.LambdaInvoke(this, 'Invoke and combine function response with task input', { - lambdaFunction: myLambda, + lambdaFunction: fn, payloadResponseOnly: true, - resultPath: '$.myLambda', + resultPath: '$.fn', }); ``` @@ -814,8 +763,8 @@ The following snippet invokes a Lambda with the task token as part of the input to the Lambda. ```ts -new tasks.LambdaInvoke(stack, 'Invoke with callback', { - lambdaFunction: myLambda, +new tasks.LambdaInvoke(this, 'Invoke with callback', { + lambdaFunction: fn, integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, payload: sfn.TaskInput.fromObject({ token: sfn.JsonPath.taskToken, @@ -830,7 +779,7 @@ Token](https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource. AWS Lambda can occasionally experience transient service errors. In this case, invoking Lambda results in a 500 error, such as `ServiceException`, `AWSLambdaException`, or `SdkClientException`. -As a best practive, the `LambdaInvoke` task will retry on those errors with an interval of 2 seconds, +As a best practice, the `LambdaInvoke` task will retry on those errors with an interval of 2 seconds, a back-off rate of 2 and 6 maximum attempts. Set the `retryOnServiceExceptions` prop to `false` to disable this behavior. @@ -843,9 +792,8 @@ Step Functions supports [AWS SageMaker](https://docs.aws.amazon.com/step-functio You can call the [`CreateTrainingJob`](https://docs.aws.amazon.com/sagemaker/latest/dg/API_CreateTrainingJob.html) API from a `Task` state. ```ts -new sfn.SageMakerCreateTrainingJob(this, 'TrainSagemaker', { +new tasks.SageMakerCreateTrainingJob(this, 'TrainSagemaker', { trainingJobName: sfn.JsonPath.stringAt('$.JobName'), - role, algorithmSpecification: { algorithmName: 'BlazingText', trainingInputMode: tasks.InputMode.FILE, @@ -860,16 +808,16 @@ new sfn.SageMakerCreateTrainingJob(this, 'TrainSagemaker', { }, }], outputDataConfig: { - s3OutputLocation: tasks.S3Location.fromBucket(s3.Bucket.fromBucketName(stack, 'Bucket', 'mybucket'), 'myoutputpath'), + s3OutputLocation: tasks.S3Location.fromBucket(s3.Bucket.fromBucketName(this, 'Bucket', 'mybucket'), 'myoutputpath'), }, resourceConfig: { instanceCount: 1, instanceType: ec2.InstanceType.of(ec2.InstanceClass.P3, ec2.InstanceSize.XLARGE2), volumeSize: cdk.Size.gibibytes(50), - }, + }, // optional: default is 1 instance of EC2 `M4.XLarge` with `10GB` volume stoppingCondition: { - maxRuntime: cdk.Duration.hours(1), - }, + maxRuntime: cdk.Duration.hours(2), + }, // optional: default is 1 hour }); ``` @@ -878,19 +826,18 @@ new sfn.SageMakerCreateTrainingJob(this, 'TrainSagemaker', { You can call the [`CreateTransformJob`](https://docs.aws.amazon.com/sagemaker/latest/dg/API_CreateTransformJob.html) API from a `Task` state. ```ts -new sfn.SageMakerCreateTransformJob(this, 'Batch Inference', { +new tasks.SageMakerCreateTransformJob(this, 'Batch Inference', { transformJobName: 'MyTransformJob', modelName: 'MyModelName', modelClientOptions: { - invocationMaxRetries: 3, // default is 0 - invocationTimeout: cdk.Duration.minutes(5), // default is 60 seconds - } - role, + invocationsMaxRetries: 3, // default is 0 + invocationsTimeout: cdk.Duration.minutes(5), // default is 60 seconds + }, transformInput: { transformDataSource: { s3DataSource: { s3Uri: 's3://inputbucket/train', - s3DataType: S3DataType.S3Prefix, + s3DataType: tasks.S3DataType.S3_PREFIX, } } }, @@ -899,7 +846,7 @@ new sfn.SageMakerCreateTransformJob(this, 'Batch Inference', { }, transformResources: { instanceCount: 1, - instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.XLARGE), } }); @@ -910,7 +857,7 @@ new sfn.SageMakerCreateTransformJob(this, 'Batch Inference', { You can call the [`CreateEndpoint`](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_CreateEndpoint.html) API from a `Task` state. ```ts -new sfn.SageMakerCreateEndpoint(this, 'SagemakerEndpoint', { +new tasks.SageMakerCreateEndpoint(this, 'SagemakerEndpoint', { endpointName: sfn.JsonPath.stringAt('$.EndpointName'), endpointConfigName: sfn.JsonPath.stringAt('$.EndpointConfigName'), }); @@ -921,7 +868,7 @@ new sfn.SageMakerCreateEndpoint(this, 'SagemakerEndpoint', { You can call the [`CreateEndpointConfig`](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_CreateEndpointConfig.html) API from a `Task` state. ```ts -new sfn.SageMakerCreateEndpointConfig(this, 'SagemakerEndpointConfig', { +new tasks.SageMakerCreateEndpointConfig(this, 'SagemakerEndpointConfig', { endpointConfigName: 'MyEndpointConfig', productionVariants: [{ initialInstanceCount: 2, @@ -937,7 +884,7 @@ new sfn.SageMakerCreateEndpointConfig(this, 'SagemakerEndpointConfig', { You can call the [`CreateModel`](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_CreateModel.html) API from a `Task` state. ```ts -new sfn.SageMakerCreateModel(this, 'Sagemaker', { +new tasks.SageMakerCreateModel(this, 'Sagemaker', { modelName: 'MyModel', primaryContainer: new tasks.ContainerDefinition({ image: tasks.DockerImage.fromJsonExpression(sfn.JsonPath.stringAt('$.Model.imageName')), @@ -952,7 +899,7 @@ new sfn.SageMakerCreateModel(this, 'Sagemaker', { You can call the [`UpdateEndpoint`](https://docs.aws.amazon.com/sagemaker/latest/APIReference/API_UpdateEndpoint.html) API from a `Task` state. ```ts -new sfn.SageMakerUpdateEndpoint(this, 'SagemakerEndpoint', { +new tasks.SageMakerUpdateEndpoint(this, 'SagemakerEndpoint', { endpointName: sfn.JsonPath.stringAt('$.Endpoint.Name'), endpointConfigName: sfn.JsonPath.stringAt('$.Endpoint.EndpointConfig'), }); @@ -965,12 +912,6 @@ Step Functions supports [Amazon SNS](https://docs.aws.amazon.com/step-functions/ You can call the [`Publish`](https://docs.aws.amazon.com/sns/latest/api/API_Publish.html) API from a `Task` state to publish to an SNS topic. ```ts -import * as sns from '@aws-cdk/aws-sns'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; - -// ... - const topic = new sns.Topic(this, 'Topic'); // Use a field from the execution data as message. @@ -1001,12 +942,12 @@ AWS Step Functions supports it's own [`StartExecution`](https://docs.aws.amazon. ```ts // Define a state machine with one Pass state -const child = new sfn.StateMachine(stack, 'ChildStateMachine', { - definition: sfn.Chain.start(new sfn.Pass(stack, 'PassState')), +const child = new sfn.StateMachine(this, 'ChildStateMachine', { + definition: sfn.Chain.start(new sfn.Pass(this, 'PassState')), }); // Include the state machine in a Task state with callback pattern -const task = new StepFunctionsStartExecution(stack, 'ChildTask', { +const task = new tasks.StepFunctionsStartExecution(this, 'ChildTask', { stateMachine: child, integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, input: sfn.TaskInput.fromObject({ @@ -1017,7 +958,7 @@ const task = new StepFunctionsStartExecution(stack, 'ChildTask', { }); // Define a second state machine with the Task state above -new sfn.StateMachine(stack, 'ParentStateMachine', { +new sfn.StateMachine(this, 'ParentStateMachine', { definition: task }); ``` @@ -1057,12 +998,6 @@ You can call the [`SendMessage`](https://docs.aws.amazon.com/AWSSimpleQueueServi to send a message to an SQS queue. ```ts -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; -import * as sqs from '@aws-cdk/aws-sqs'; - -// ... - const queue = new sqs.Queue(this, 'Queue'); // Use a field from the execution data as message. diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-stepfunctions-tasks/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..11558a599e5f4 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/rosetta/default.ts-fixture @@ -0,0 +1,37 @@ +// Fixture with packages imported, but nothing else +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as ddb from '@aws-cdk/aws-dynamodb'; +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 s3 from '@aws-cdk/aws-s3'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as sns from '@aws-cdk/aws-sns'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; + +class Fixture extends cdk.Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const fn = new lambda.Function(this, 'lambdaFunction', { + code: lambda.Code.fromInline(`exports.handler = async () => { + return { "hello world"}; + `), + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + }); + + const myTable = new ddb.Table(this, 'Messages', { + tableName: 'my-table', + partitionKey: { + name: 'MessageId', + type: ddb.AttributeType.STRING, + }, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/rosetta/with-batch-job.ts-fixture b/packages/@aws-cdk/aws-stepfunctions-tasks/rosetta/with-batch-job.ts-fixture new file mode 100644 index 0000000000000..47672ba140841 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/rosetta/with-batch-job.ts-fixture @@ -0,0 +1,38 @@ +// Fixture with packages imported, but nothing else +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; +import * as batch from '@aws-cdk/aws-batch'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as path from 'path'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { + isDefault: true, + }); + + const batchQueue = new batch.JobQueue(this, 'JobQueue', { + computeEnvironments: [ + { + order: 1, + computeEnvironment: new batch.ComputeEnvironment(this, 'ComputeEnv', { + computeResources: { vpc }, + }), + }, + ], + }); + + const batchJobDefinition = new batch.JobDefinition(this, 'JobDefinition', { + container: { + image: ecs.ContainerImage.fromAsset(path.resolve(__dirname, 'batchjob-image')), + }, + }); + + /// here + } +} diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index d717c69b9db96..bcbb1c7193e68 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -36,7 +36,7 @@ "fast-check": "^2.13.0", "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.5.2" + "ts-jest": "^26.5.3" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index ce9716626baee..f9b2e79888365 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -367,7 +367,7 @@ "cdk-integ-tools": "0.0.0", "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.5.2" + "ts-jest": "^26.5.3" }, "keywords": [ "aws", diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/find-in-map-with-dynamic-mapping.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/find-in-map-with-dynamic-mapping.json new file mode 100644 index 0000000000000..aedf3250272fc --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/find-in-map-with-dynamic-mapping.json @@ -0,0 +1,30 @@ +{ + "Parameters": { + "Stage": { + "Type": "String", + "AllowedValues": ["beta"], + "Default": "beta" + } + }, + "Mappings": { + "beta": { + "region": { + "key1": "name" + } + } + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::FindInMap": [ + { "Ref": "Stage" }, + "region", + "key1" + ] + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts index 873add618b9d0..34b403a2e7a99 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -746,6 +746,14 @@ describe('CDK Include', () => { }).toThrow(/Mapping with name 'NonExistentMapping' was not found in the template/); }); + test('can ingest a template that uses Fn::FindInMap with the first argument being a dynamic reference', () => { + includeTestTemplate(stack, 'find-in-map-with-dynamic-mapping.json'); + + expect(stack).toMatchTemplate( + loadTestFileToJsObject('find-in-map-with-dynamic-mapping.json'), + ); + }); + test('handles renaming Mapping references', () => { const cfnTemplate = includeTestTemplate(stack, 'only-mapping-and-bucket.json'); const someMapping = cfnTemplate.getMapping('SomeMapping'); diff --git a/packages/@aws-cdk/core/lib/cfn-parse.ts b/packages/@aws-cdk/core/lib/cfn-parse.ts index 1a5c245b61c4e..93376ae19d365 100644 --- a/packages/@aws-cdk/core/lib/cfn-parse.ts +++ b/packages/@aws-cdk/core/lib/cfn-parse.ts @@ -566,11 +566,19 @@ export class CfnParser { case 'Fn::FindInMap': { const value = this.parseValue(object[key]); // the first argument to FindInMap is the mapping name - const mapping = this.finder.findMapping(value[0]); - if (!mapping) { - throw new Error(`Mapping used in FindInMap expression with name '${value[0]}' was not found in the template`); + let mappingName: string; + if (Token.isUnresolved(value[0])) { + // the first argument can be a dynamic expression like Ref: Param; + // if it is, we can't find the mapping in advance + mappingName = value[0]; + } else { + const mapping = this.finder.findMapping(value[0]); + if (!mapping) { + throw new Error(`Mapping used in FindInMap expression with name '${value[0]}' was not found in the template`); + } + mappingName = mapping.logicalId; } - return Fn._findInMap(mapping.logicalId, value[1], value[2]); + return Fn._findInMap(mappingName, value[1], value[2]); } case 'Fn::Select': { const value = this.parseValue(object[key]); diff --git a/packages/@aws-cdk/core/lib/duration.ts b/packages/@aws-cdk/core/lib/duration.ts index 098744af2d0b8..b1c675d3ce722 100644 --- a/packages/@aws-cdk/core/lib/duration.ts +++ b/packages/@aws-cdk/core/lib/duration.ts @@ -1,4 +1,4 @@ -import { Token } from './token'; +import { Token, Tokenization } from './token'; /** * Represents a length of time. @@ -252,6 +252,28 @@ export class Duration { } return ret; } + + /** + * Checks if duration is a token or a resolvable object + */ + public isUnresolved() { + return Token.isUnresolved(this.amount); + } + + /** + * Returns unit of the duration + */ + public unitLabel() { + return this.unit.label; + } + + /** + * Returns stringified number of duration + */ + public formatTokenToNumber(): string { + const number = Tokenization.stringifyNumber(this.amount); + return `${number} ${this.unit.label}`; + } } /** diff --git a/packages/@aws-cdk/core/test/duration.test.ts b/packages/@aws-cdk/core/test/duration.test.ts index 70b7cb786e344..75b92c76924c2 100644 --- a/packages/@aws-cdk/core/test/duration.test.ts +++ b/packages/@aws-cdk/core/test/duration.test.ts @@ -168,6 +168,32 @@ nodeunitShim({ test.done(); }, + + 'get unit label from duration'(test: Test) { + test.equal(Duration.minutes(Lazy.number({ produce: () => 10 })).unitLabel(), 'minutes'); + test.equal(Duration.minutes(62).unitLabel(), 'minutes'); + test.equal(Duration.seconds(10).unitLabel(), 'seconds'); + test.equal(Duration.millis(1).unitLabel(), 'millis'); + test.equal(Duration.hours(1000).unitLabel(), 'hours'); + test.equal(Duration.days(2).unitLabel(), 'days'); + test.done(); + }, + + 'format number token to number'(test: Test) { + const stack = new Stack(); + const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 10 })); + test.equal(stack.resolve(lazyDuration.formatTokenToNumber()), '10 minutes'); + test.equal(Duration.hours(10).formatTokenToNumber(), '10 hours'); + test.equal(Duration.days(5).formatTokenToNumber(), '5 days'); + test.done(); + }, + + 'duration is unresolved'(test: Test) { + const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 10 })); + test.equal(lazyDuration.isUnresolved(), true); + test.equal(Duration.hours(10).isUnresolved(), false); + test.done(); + }, }); function floatEqual(test: Test, actual: number, expected: number) { diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 0044f79eda383..97363c57b7918 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -84,7 +84,7 @@ "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "fs-extra": "^9.1.0", - "nock": "^13.0.9", + "nock": "^13.0.10", "pkglint": "0.0.0", "sinon": "^9.2.4" }, diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 512ee5e97c92b..15e6da27613d3 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -173,7 +173,7 @@ class MyPipelineStack extends Stack { } const app = new App(); -new MyPipelineStack(this, 'PipelineStack', { +new MyPipelineStack(app, 'PipelineStack', { env: { account: '111111111111', region: 'eu-west-1', @@ -198,7 +198,8 @@ const codePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { ], }); -const cdkPipeline = new CdkPipeline(this, 'CdkPipeline', { +const app = new App(); +const cdkPipeline = new CdkPipeline(app, 'CdkPipeline', { codePipeline, cloudAssemblyArtifact, }); diff --git a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts index 528cd8463f05e..dc7f8ff586449 100644 --- a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts +++ b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts @@ -158,5 +158,6 @@ export const APPMESH_ECR_ACCOUNTS: { [region: string]: string } = { 'me-south-1': '772975370895', 'ap-east-1': '856666278305', + 'af-south-1': '924023996002', }; diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index a4d825852a613..f70aec783eff1 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -41,7 +41,7 @@ "jest": "^26.6.3", "monocdk": "0.0.0", "pkglint": "0.0.0", - "ts-jest": "^26.5.2" + "ts-jest": "^26.5.3" }, "dependencies": { "@aws-cdk/cloudformation-diff": "0.0.0" diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 785087c8fb196..71a094e6e72ca 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -39,7 +39,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/core": "0.0.0", - "@octokit/rest": "^18.3.1", + "@octokit/rest": "^18.3.2", "@types/archiver": "^5.1.0", "@types/fs-extra": "^8.1.1", "@types/glob": "^7.1.3", @@ -59,10 +59,10 @@ "jest": "^26.6.3", "make-runnable": "^1.3.8", "mockery": "^2.1.0", - "nock": "^13.0.9", + "nock": "^13.0.10", "pkglint": "0.0.0", "sinon": "^9.2.4", - "ts-jest": "^26.5.2", + "ts-jest": "^26.5.3", "ts-mock-imports": "^1.3.3", "xml-js": "^1.6.11" }, diff --git a/packages/awslint/package.json b/packages/awslint/package.json index 3a5e17066742b..3be540173745f 100644 --- a/packages/awslint/package.json +++ b/packages/awslint/package.json @@ -16,11 +16,11 @@ "awslint": "bin/awslint" }, "dependencies": { - "@jsii/spec": "^1.23.0", + "@jsii/spec": "^1.24.0", "camelcase": "^6.2.0", "colors": "^1.4.0", "fs-extra": "^9.1.0", - "jsii-reflect": "^1.23.0", + "jsii-reflect": "^1.24.0", "yargs": "^16.2.0" }, "devDependencies": { diff --git a/packages/cdk-dasm/package.json b/packages/cdk-dasm/package.json index a60843d4454b7..067cf1694ff98 100644 --- a/packages/cdk-dasm/package.json +++ b/packages/cdk-dasm/package.json @@ -26,7 +26,7 @@ }, "license": "Apache-2.0", "dependencies": { - "codemaker": "^1.23.0", + "codemaker": "^1.24.0", "yaml": "1.10.0" }, "devDependencies": { diff --git a/packages/decdk/package.json b/packages/decdk/package.json index 2fa2feeaabae7..8b8b17d52f621 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -210,7 +210,7 @@ "@aws-cdk/yaml-cfn": "0.0.0", "constructs": "^3.2.0", "fs-extra": "^9.1.0", - "jsii-reflect": "^1.23.0", + "jsii-reflect": "^1.24.0", "jsonschema": "^1.4.0", "yaml": "1.10.0", "yargs": "^16.2.0" @@ -221,7 +221,7 @@ "@types/yaml": "1.9.7", "@types/yargs": "^15.0.13", "jest": "^26.6.3", - "jsii": "^1.23.0" + "jsii": "^1.24.0" }, "keywords": [ "aws", diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index c9bd2823410f3..645d6eb54b454 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -51,13 +51,13 @@ "eslint-plugin-jest": "^24.1.5", "fs-extra": "^9.1.0", "jest": "^26.6.3", - "jsii": "^1.23.0", - "jsii-pacmak": "^1.23.0", - "markdownlint-cli": "^0.27.0", + "jsii": "^1.24.0", + "jsii-pacmak": "^1.24.0", + "markdownlint-cli": "^0.27.1", "nodeunit": "^0.11.3", "nyc": "^15.1.0", "semver": "^7.3.4", - "ts-jest": "^26.5.2", + "ts-jest": "^26.5.3", "typescript": "~3.9.9", "yargs": "^16.2.0", "yarn-cling": "0.0.0" diff --git a/tools/cfn2ts/package.json b/tools/cfn2ts/package.json index bcee4f73d7020..6bc7f4ebb4d58 100644 --- a/tools/cfn2ts/package.json +++ b/tools/cfn2ts/package.json @@ -30,7 +30,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-cdk/cfnspec": "0.0.0", - "codemaker": "^1.23.0", + "codemaker": "^1.24.0", "fast-json-patch": "^3.0.0-1", "fs-extra": "^9.1.0", "yargs": "^16.2.0" diff --git a/tools/yarn-cling/lib/index.ts b/tools/yarn-cling/lib/index.ts index 44a09a09fcb8d..38a766c27d7e5 100644 --- a/tools/yarn-cling/lib/index.ts +++ b/tools/yarn-cling/lib/index.ts @@ -1,6 +1,6 @@ -import * as lockfile from '@yarnpkg/lockfile'; -import { promises as fs } from 'fs'; +import { promises as fs, exists } from 'fs'; import * as path from 'path'; +import * as lockfile from '@yarnpkg/lockfile'; import { hoistDependencies } from './hoisting'; import { PackageJson, PackageLock, PackageLockEntry, PackageLockPackage, YarnLock } from './types'; @@ -42,7 +42,7 @@ export async function generateShrinkwrap(options: ShrinkwrapOptions): Promise, yarnLock: YarnLock, rootDir = await fs.realpath(rootDir); for (const [depName, versionRange] of Object.entries(deps)) { - const depPkgJsonFile = require.resolve(`${depName}/package.json`, { paths: [rootDir] }); + const depDir = await findPackageDir(depName, rootDir); + const depPkgJsonFile = path.join(depDir, 'package.json'); const depPkgJson = await loadPackageJson(depPkgJsonFile); - const depDir = path.dirname(depPkgJsonFile); const yarnKey = `${depName}@${versionRange}`; // Sanity check @@ -150,4 +150,40 @@ export function formatPackageLock(entry: PackageLockEntry) { recurse([...names, depName], depEntry); } } +} + +/** + * Find package directory + * + * Do this by walking upwards in the directory tree until we find + * `/node_modules//package.json`. + * + * ------- + * + * Things that we tried but don't work: + * + * 1. require.resolve(`${depName}/package.json`, { paths: [rootDir] }); + * + * Breaks with ES Modules if `package.json` has not been exported, which is + * being enforced starting Node12. + * + * 2. findPackageJsonUpwardFrom(require.resolve(depName, { paths: [rootDir] })) + * + * Breaks if a built-in NodeJS package name conflicts with an NPM package name + * (in Node15 `string_decoder` is introduced...) + */ +async function findPackageDir(depName: string, rootDir: string) { + let prevDir; + let dir = rootDir; + while (dir !== prevDir) { + const candidateDir = path.join(dir, 'node_modules', depName); + if (await new Promise(ok => exists(path.join(candidateDir, 'package.json'), ok))) { + return candidateDir; + } + + prevDir = dir; + dir = path.dirname(dir); // dirname('/') -> '/', dirname('c:\\') -> 'c:\\' + } + + throw new Error(`Did not find '${depName}' upwards of '${rootDir}'`); } \ No newline at end of file diff --git a/version.v1.json b/version.v1.json index aa566c6d404ff..c2a1515792517 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.91.0" + "version": "1.92.0" } diff --git a/yarn.lock b/yarn.lock index a882be99918c4..9e33a1efe99be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -587,10 +587,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@jsii/spec@^1.23.0": - version "1.23.0" - resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.23.0.tgz#c554cc77d0206f00ba4c101e96cb89d4ecec2985" - integrity sha512-+8Df/m/SRnThSAARlfxknwq1iWnCc2I1kThopuGQiz+peTRMoDVXaH7ZKT+54iZFIrbSf9Pjetho8g0/AZwrKg== +"@jsii/spec@^1.24.0": + version "1.24.0" + resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.24.0.tgz#59dd43af7bb65074b39bd457a98f4ab3cb385bd3" + integrity sha512-Km0va0ZBlzWPOijsNlo7SwozYT6Ej9h01xXYtBmoHw2CLccofOEQLV2Ig3+ydhU+aTW5yLKJrVPsAjJoaaBAgA== dependencies: jsonschema "^1.4.0" @@ -1350,10 +1350,10 @@ "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" -"@octokit/openapi-types@^5.2.1": - version "5.2.1" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-5.2.1.tgz#5e846f86104aef96ace20091d8afb6be27979d8a" - integrity sha512-Bf7MBvQ1nMpv15ANaQtRBsC7YnwQFPM0eUztp3luQs9L6sBEiQ6ArM1Wx5CG+N7tXETtd0oE0DMcU4wbLlCZIw== +"@octokit/openapi-types@^5.2.2": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-5.2.2.tgz#1590c118a131031610faffd4222ae54915e2b82d" + integrity sha512-b3nHy/0uufJJsaZERwZM0syLRO6gfr6vvBPLewQxBKzzbhGDx1ygTyoELMNADD7mIPPzGMqbfdCeJTSeZueZwA== "@octokit/plugin-enterprise-rest@^6.0.1": version "6.0.1" @@ -1387,12 +1387,12 @@ "@octokit/types" "^2.0.1" deprecation "^2.3.1" -"@octokit/plugin-rest-endpoint-methods@4.13.1": - version "4.13.1" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.13.1.tgz#d8c807bbd0e187ac903620f53321e2634818bb30" - integrity sha512-T9YhQqpbO9Onmg+FYk09uci9pfChg8CZR9GBaPJWj+bDSzictW1xnU0NtCSSKKyrwvpW/opu7CtuDSs/HF1Syg== +"@octokit/plugin-rest-endpoint-methods@4.13.2": + version "4.13.2" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.13.2.tgz#4e1fa30742a7bb1f0fe99e2f8e2b21f7aa8b7eb5" + integrity sha512-pnn0lGE05nqZ+EZuZgJBffJ4QRAlrlvg3LBFjCKHqUUWCI3PMqZ8kPJOxFj0R3B3D5hoGambtEIpCnaRD6nRJw== dependencies: - "@octokit/types" "^6.11.1" + "@octokit/types" "^6.11.2" deprecation "^2.3.1" "@octokit/request-error@^1.0.2": @@ -1449,15 +1449,15 @@ once "^1.4.0" universal-user-agent "^4.0.0" -"@octokit/rest@^18.3.1": - version "18.3.1" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.3.1.tgz#6680331e941c422dbff0e758a9bd3dc4edcbd2db" - integrity sha512-g57ebsk7dtbLjiPBgEYDAiDTsyQM9kvlIt0J5UN6OSjG82K6fQQck6HXPpwcyNIDqbN7lIaWr3nsz56jBfI6qg== +"@octokit/rest@^18.3.2": + version "18.3.2" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.3.2.tgz#dae61ff8c70e3f968e182919f36f1338163c8ea8" + integrity sha512-TSTI47/jLqdq8qvc/a/P/VApBal7QHeISPZ8a1v7ma8NN/YdU5HRTSCb15+IvzUeuM7Iagp0aG+ypvQXJgmgQw== dependencies: "@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.13.1" + "@octokit/plugin-rest-endpoint-methods" "4.13.2" "@octokit/types@^2.0.0", "@octokit/types@^2.0.1": version "2.16.2" @@ -1466,12 +1466,12 @@ dependencies: "@types/node" ">= 8" -"@octokit/types@^6.0.3", "@octokit/types@^6.11.0", "@octokit/types@^6.11.1", "@octokit/types@^6.7.1": - version "6.11.1" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.11.1.tgz#54ece128029526fa99bd71e757b9e35478403d95" - integrity sha512-UiSRTG2lrFbMUMwhKNR0uSV33Fzv4bNu1n5iFuuNOg80XCh0VYNhR4TQWgrkLhVxdDuej6s61aP3eQvVm6K6uA== +"@octokit/types@^6.0.3", "@octokit/types@^6.11.0", "@octokit/types@^6.11.2", "@octokit/types@^6.7.1": + version "6.11.2" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.11.2.tgz#43973dc29cdf59bf9d5f3ab0d16275c4b4a6eb8d" + integrity sha512-EKQRFZU/oOfUlqk9ntLIE5UO/bcOx8exFpdXGBciJP90f05me3mza0sacIpqVqmiIQP3nJsBjnZHMmtijE5XwQ== dependencies: - "@octokit/openapi-types" "^5.2.1" + "@octokit/openapi-types" "^5.2.2" "@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.1": version "1.8.2" @@ -1605,7 +1605,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@26.x", "@types/jest@^26.0.20": +"@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== @@ -1685,9 +1685,9 @@ integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== "@types/prettier@^2.0.0": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.1.tgz#374e31645d58cb18a07b3ecd8e9dede4deb2cccd" - integrity sha512-DxZZbyMAM9GWEzXL+BMZROWz9oo6A9EilwwOMET2UVu2uZTqMWS5S69KVtuVKaRjCUpcrOXRalet86/OpG4kqw== + version "2.2.2" + resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.2.tgz#e2280c89ddcbeef340099d6968d8c86ba155fdf6" + integrity sha512-i99hy7Ki19EqVOl77WplDrvgNugHnsSjECVR/wUrzw2TJXz1zlUfT2ngGckR6xN7yFYaijsMAqPkOLx9HgUqHg== "@types/promptly@^3.0.1": version "3.0.1" @@ -2285,9 +2285,9 @@ aws-sdk-mock@^5.1.0: traverse "^0.6.6" aws-sdk@^2.596.0, aws-sdk@^2.637.0, aws-sdk@^2.848.0: - version "2.854.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.854.0.tgz#531525cdfdfc774232d86619d2c2c2ae3e1a71a8" - integrity sha512-Ex/YJcRB4avEW64UZ7uJ7aBxN8qFPuoSaG7YpLWUGJv+p9Vsqm0v0IqmMo7O5/s2LpIXRG8mLi3UbNZYz2iGhg== + version "2.856.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.856.0.tgz#a82719952949bdfda73832b465321a49577cf784" + integrity sha512-B9uRDhIxlmaz5GIDC2Q+OgVMnsPFzuu1HknWedpTre6ARG+Ukm8KQ3VA5MhsBtSm61X5u1Uwk5AlEv+feLaShA== dependencies: buffer "4.9.2" events "1.1.1" @@ -2684,9 +2684,9 @@ camelcase@^6.0.0, camelcase@^6.2.0: integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== caniuse-lite@^1.0.30001181: - version "1.0.30001194" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001194.tgz#3d16ff3d734a5a7d9818402c28b1f636c5be5bed" - integrity sha512-iDUOH+oFeBYk5XawYsPtsx/8fFpndAPUQJC7gBTfxHM8xw5nOZv7ceAD4frS1MKCLUac7QL5wdAJiFQlDRjXlA== + version "1.0.30001196" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001196.tgz#00518a2044b1abf3e0df31fadbe5ed90b63f4e64" + integrity sha512-CPvObjD3ovWrNBaXlAIGWmg2gQQuJ5YhuciUOjPRox6hIQttu8O+b51dx6VIpIY9ESd2d0Vac1RKpICdG4rGUg== capture-exit@^2.0.0: version "2.0.0" @@ -2868,10 +2868,10 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -codemaker@^1.23.0: - version "1.23.0" - resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.23.0.tgz#2d2168e70829496cff0549983b6ea7b7f843c1a3" - integrity sha512-VpHeRuvibqJbayWj9xHb15yKRLBOJUrZuhsmH868m6xGz5FXwLD1aUdSY9rqaNI0df8FfFyCYhk5f/A+bi0/wg== +codemaker@^1.24.0: + version "1.24.0" + resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.24.0.tgz#6b8d4d106fd275713a078bf62fc5c6ebcaa65343" + integrity sha512-oKi5BNSyH0LBtFoxWKRvWnTut8NRdRgOzIF6/YKCaNnVECqq0oMqUpEBKNgcS+sOxfJfI/tkORpSdNGGVE0tmA== dependencies: camelcase "^6.2.0" decamelize "^5.0.0" @@ -2977,10 +2977,10 @@ component-emitter@^1.2.1: resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== -compress-commons@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.0.2.tgz#d6896be386e52f37610cef9e6fa5defc58c31bd7" - integrity sha512-qhd32a9xgzmpfoga1VQEiLEwdKZ6Plnpx5UCgIsf89FSolyJ7WnifY4Gtjgv5WR6hWAyRaHxC5MiEhU/38U70A== +compress-commons@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.0.tgz#25ec7a4528852ccd1d441a7d4353cd0ece11371b" + integrity sha512-ofaaLqfraD1YRTkrRKPCrGJ1pFeDG/MVCkVVV2FNGeWquSlqw5wOrwOfPQ1xF2u+blpeWASie5EubHz+vsNIgA== dependencies: buffer-crc32 "^0.2.13" crc32-stream "^4.0.1" @@ -3026,9 +3026,9 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0: integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= constructs@^3.2.0: - version "3.3.48" - resolved "https://registry.yarnpkg.com/constructs/-/constructs-3.3.48.tgz#56446b7bd8da8023f33a2e4c1bc7d2df3971e6eb" - integrity sha512-5AKrTtmiioQWloJ3WRFZb0/uR1lrRboaVE9go++XZltvRnZkN2/kQjaZ0gtFxynU5u5k9mWVtk8mNcgJ9yoRbQ== + version "3.3.55" + resolved "https://registry.yarnpkg.com/constructs/-/constructs-3.3.55.tgz#ee54bd2202d8d72d9ce10694f055054d8cffef1f" + integrity sha512-PKlPeHnIN7EAdanoVpwUnj6Zb5Zhgb6lCQ1YTES52puksbw34TG7oqO2kzmTVTK3MVDMUdhCDKyroaVmNOtiiw== contains-path@^0.1.0: version "0.1.0" @@ -3782,9 +3782,9 @@ ejs@^2.5.2: integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== electron-to-chromium@^1.3.649: - version "1.3.677" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.677.tgz#b5d586b0d1976c97cc7e95262677ac5944199513" - integrity sha512-Tcmk+oKQgpjcM+KYanlkd76ZtpzalkpUULnlJDP6vjHtR7UU564IM9Qv5DxqHZNBQjzXm6mkn7Y8bw2OoE3FmQ== + version "1.3.680" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.680.tgz#88cc44bd2a85b46cf7521f714db57dd74d0cd488" + integrity sha512-XBACJT9RdpdWtoMXQPR8Be3ZtmizWWbxfw8cY2b5feUwiDO3FUl8qo4W2jXoq/WnnA3xBRqafu1XbpczqyUvlA== emittery@^0.7.1: version "0.7.2" @@ -3855,9 +3855,9 @@ error-ex@^1.2.0, error-ex@^1.3.1: is-arrayish "^0.2.1" es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2: - version "1.18.0-next.3" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.3.tgz#56bc8b5cc36b2cca25a13be07f3c02c2343db6b7" - integrity sha512-VMzHx/Bczjg59E6jZOQjHeN3DEoptdhejpARgflAViidlqSpjdq9zA6lKwlhRRs/lOw1gHJv2xkkSFRgvEwbQg== + version "1.18.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4" + integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== dependencies: call-bind "^1.0.2" es-to-primitive "^1.2.1" @@ -3921,10 +3921,10 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -esbuild@^0.8.54: - version "0.8.54" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.54.tgz#2f32ff80e95c69a0f25b799d76a27c05e2857cdf" - integrity sha512-DJH38OiTgXJxFb/EhHrCrY8eGmtdkTtWymHpN9IYN9AF+4jykT0dQArr7wzFejpVbaB0TMIq2+vfNRWr3LXpvw== +esbuild@^0.8.55: + version "0.8.55" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.55.tgz#4bf949c44db4ffc2a206ac0c002e8e94eecff7d5" + integrity sha512-mM/s7hjYe5mQR+zAWOM5JVrCtYCke182E9l1Bbs6rG5EDP3b1gZF9sHZka53PD/iNt6OccymVZRWkTtBfcKW4w== escalade@^3.1.1: version "3.1.1" @@ -6250,65 +6250,65 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsii-diff@^1.23.0: - version "1.23.0" - resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.23.0.tgz#57fc832155e4739fbfc7a235b177c305e9cb6f67" - integrity sha512-1g41UE2yJK8E50eDo4JSJQZRlfmUzWDw0D7DF40Z6upbay3jQKWjerXbJmD9RvJkD6I8khAoEnI77daw1AgQTw== +jsii-diff@^1.24.0: + version "1.24.0" + resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.24.0.tgz#97375083661a9cdd931764b526184bb5eb192497" + integrity sha512-Wkj/fyAmtNsaLSr5r6abgm5nnU7aHnlaqj//gG0azum7+TkbrmNj9LaxwXYJWT2fROCmP/fS8/jJta5U3wAi7Q== dependencies: - "@jsii/spec" "^1.23.0" + "@jsii/spec" "^1.24.0" fs-extra "^9.1.0" - jsii-reflect "^1.23.0" + jsii-reflect "^1.24.0" log4js "^6.3.0" typescript "~3.9.9" yargs "^16.2.0" -jsii-pacmak@^1.23.0: - version "1.23.0" - resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.23.0.tgz#aedc0f17dc420da5be9446189eae5096fe170bd3" - integrity sha512-LchV8amHqWHjypbPLDZqdLkmp0rVYJYhFbS88sApvO4TaDdh0C3KnbEjNf3Jn/JpPXxd2PUNtCw8D9fDy7zwVw== +jsii-pacmak@^1.24.0: + version "1.24.0" + resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.24.0.tgz#f16cb329ded0b2ce435846013910c811c6e29c34" + integrity sha512-8L5AOOy2LU3Y8tLy+KBTdmtxfu+Kn6g54htj+d1g3dVCxApC/G83C1DudhkYCxM3gDavsLPHhG6+fyopdTVV5A== dependencies: - "@jsii/spec" "^1.23.0" + "@jsii/spec" "^1.24.0" clone "^2.1.2" - codemaker "^1.23.0" + codemaker "^1.24.0" commonmark "^0.29.3" escape-string-regexp "^4.0.0" fs-extra "^9.1.0" - jsii-reflect "^1.23.0" - jsii-rosetta "^1.23.0" + jsii-reflect "^1.24.0" + jsii-rosetta "^1.24.0" semver "^7.3.4" spdx-license-list "^6.4.0" xmlbuilder "^15.1.1" yargs "^16.2.0" -jsii-reflect@^1.23.0: - version "1.23.0" - resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.23.0.tgz#fd046e25e4d89887bc0ea7416d68e42568c6689e" - integrity sha512-jxQnz5hm5tk7l85iWgfyuxnB+mpzmj/y96AIkSbbOfHDeHpLG7h+yOROZoe2K8tUcHKamA70p+gNcZOETZP3OA== +jsii-reflect@^1.24.0: + version "1.24.0" + resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.24.0.tgz#61589ba10bc4bceeaeb68dd9e13acc22ac4b4e73" + integrity sha512-LWWReRtLhmyUMRD5NOFDV+HJHP/ChHRa6alccSPU9vTL5tm9HtMW0oO2XaVj4a2YPujvQ+sH7APzndj60Qgzqw== dependencies: - "@jsii/spec" "^1.23.0" + "@jsii/spec" "^1.24.0" colors "^1.4.0" fs-extra "^9.1.0" - oo-ascii-tree "^1.23.0" + oo-ascii-tree "^1.24.0" yargs "^16.2.0" -jsii-rosetta@^1.23.0: - version "1.23.0" - resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.23.0.tgz#fd62dbf84b16cb119c80aca4397ab5d6ee02f071" - integrity sha512-LBFq1LBrq97MUU5T9sgrIPq0wMWUCsUt3tnZEpjIDXPHYptVVn7HmKbu5PjG47GbNTOcsY/nP2r7N3RFrG881g== +jsii-rosetta@^1.24.0: + version "1.24.0" + resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.24.0.tgz#f0c674e6fa88d449360171400a46c556cde184bd" + integrity sha512-BMYxIjYG62wctUZFjYc5xKPNTgzTRRw2Fp8R9p4o0VeFE224ntyHgy9y7oKuCu+K1ev917NRoCLj7f2tyGTMAg== dependencies: - "@jsii/spec" "^1.23.0" + "@jsii/spec" "^1.24.0" commonmark "^0.29.3" fs-extra "^9.1.0" typescript "~3.9.9" xmldom "^0.4.0" yargs "^16.2.0" -jsii@^1.23.0: - version "1.23.0" - resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.23.0.tgz#12e04a565114024bc6721f6482e9a362bbf50926" - integrity sha512-vwwFEt+7wXtex8G1ztWIuOPDhahZA/a+XvtiucZYnIO7VwkmeqAxrgwdBIrQoH2GJKYs55kMnDa4+yfnTGcOgw== +jsii@^1.24.0: + version "1.24.0" + resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.24.0.tgz#48e156b77f54d067520e0789f75d70c883be058a" + integrity sha512-ROCFFFdbs2o8OgQEWvpMY/UMXshndkGr8TunG0NQx8kQfONYeCjgNFqMcbcyXuhlR+DI1MUGVVOzw6zc9geyGA== dependencies: - "@jsii/spec" "^1.23.0" + "@jsii/spec" "^1.24.0" case "^1.6.3" colors "^1.4.0" deep-equal "^2.0.5" @@ -6880,10 +6880,10 @@ markdown-it@12.0.4: mdurl "^1.0.1" uc.micro "^1.0.5" -markdownlint-cli@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/markdownlint-cli/-/markdownlint-cli-0.27.0.tgz#d89264319ef7ae6a02e0b2fd472fbbb11bde47f7" - integrity sha512-lci/GD1V2aysJOpJFXfIt5KlpF62Rw+7hUfGg3ZwkLBBeNuUwLZj/IomQ924m00Ido/tPGbFObelKlZaUlETpQ== +markdownlint-cli@^0.27.1: + version "0.27.1" + resolved "https://registry.yarnpkg.com/markdownlint-cli/-/markdownlint-cli-0.27.1.tgz#8fa095eea94936b6dea891f9db7f269c60e6d6fa" + integrity sha512-p1VV6aSbGrDlpUWzHizAnSNEQAweVR3qUI/AIUubxW7BGPXziSXkIED+uRtSohUlRS/jmqp3Wi4es5j6fIrdeQ== dependencies: commander "~7.1.0" deep-extend "~0.6.0" @@ -7273,10 +7273,10 @@ nise@^4.0.4: just-extend "^4.0.2" path-to-regexp "^1.7.0" -nock@^13.0.9: - version "13.0.9" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.9.tgz#32b9f6408a71991b25f044109cac92ee556c8539" - integrity sha512-SoGx/J0SsZPOdBFrBC9PP6NwaEgOBQIRPbsOsO9q+OwOPWc5eT6wALSxn3ZNE4Fv2ImIUXM4Hv/07rjq/uWDew== +nock@^13.0.10: + version "13.0.10" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.10.tgz#a9a015269408d56ec9ac57fe3ede86b0a83ecc6a" + integrity sha512-AvUO/tbiWVBjlC3WsuIutPXltPbPmHWfcLwDSYzykKBJhOeo9eZPczo8n9aV4AHHCgpeL70zBXLwiSE+mzx89g== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" @@ -7664,10 +7664,10 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -oo-ascii-tree@^1.23.0: - version "1.23.0" - resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.23.0.tgz#b7a8ece5d313a420689a6a9d728a2696e8991e40" - integrity sha512-+T0rUcBAvJaEXbdCe3p8A7qjJ82CRGSgfUSvkGscH2F0lFqrELN1ey0UGDf0UV7C/ivIFzct5DYJAhrVvFbeUw== +oo-ascii-tree@^1.24.0: + version "1.24.0" + resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.24.0.tgz#4eae1b2c2c6ba5bd83129c54d4b82443316dd378" + integrity sha512-rJYspYyrr2lDCDnybz4/70eml5cen98r4u2uw8sGodROePXiKKE+2Al8tfiS6uYx7vUEozEHCLoNQ/2jpxa7gw== opener@^1.5.1: version "1.5.2" @@ -9863,12 +9863,11 @@ trivial-deferred@^1.0.1: resolved "https://registry.yarnpkg.com/trivial-deferred/-/trivial-deferred-1.0.1.tgz#376d4d29d951d6368a6f7a0ae85c2f4d5e0658f3" integrity sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM= -ts-jest@^26.5.2: - version "26.5.2" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.2.tgz#5281d6b44c2f94f71205728a389edc3d7995b0c4" - integrity sha512-bwyJ2zJieSugf7RB+o8fgkMeoMVMM2KPDE0UklRLuACxjwJsOrZNo6chrcScmK33YavPSwhARffy8dZx5LJdUQ== +ts-jest@^26.5.3: + version "26.5.3" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.3.tgz#a6ee00ba547be3b09877550df40a1465d0295554" + integrity sha512-nBiiFGNvtujdLryU7MiMQh1iPmnZ/QvOskBbD2kURiI1MwqvxlxNnaAB/z9TbslMqCsSbu5BXvSSQPc5tvHGeA== dependencies: - "@types/jest" "26.x" bs-logger "0.x" buffer-from "1.x" fast-json-stable-stringify "2.x" @@ -10673,10 +10672,10 @@ yocto-queue@^0.1.0: 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" - integrity sha512-a65wQ3h5gcQ/nQGWV1mSZCEzCML6EK/vyVPcrPNynySP1j3VBbQKh3nhC8CbORb+jfl2vXvh56Ul5odP1bAHqw== + version "4.1.0" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79" + integrity sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A== dependencies: archiver-utils "^2.1.0" - compress-commons "^4.0.2" + compress-commons "^4.1.0" readable-stream "^3.6.0"