diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml index d36f6255bbc7d..c289f5381995c 100644 --- a/.github/workflows/auto-approve.yml +++ b/.github/workflows/auto-approve.yml @@ -1,13 +1,17 @@ # 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: if: > - contains(github.event.pull_request.labels.*.name, 'pr/auto-approve') && - contains(['aws-cdk-automation', 'dependabot[bot]', 'dependabot-preview[bot]'], github.event.pull_request.user.login) + contains(github.event.pull_request.labels.*.name, 'pr/auto-approve') && + (github.event.pull_request.user.login == 'aws-cdk-automation' + || github.event.pull_request.user.login == 'dependabot[bot]' + || github.event.pull_request.user.login == 'dependabot-preview[bot]') runs-on: ubuntu-latest steps: - uses: hmarr/auto-approve-action@v2.0.0 diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 22f20538795c7..01afcd841138d 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -54,8 +54,8 @@ jobs: {"keywords":["(@aws-cdk/aws-cloudfront)","(aws-cloudfront)","(cloudfront)","(cloud front)"],"labels":["@aws-cdk/aws-cloudfront"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-cloudfront-origins)","(aws-cloudfront-origins)","(cloudfront-origins)","(cloudfront origins)"],"labels":["@aws-cdk/aws-cloudfront-origins"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-cloudtrail)","(aws-cloudtrail)","(cloudtrail)","(cloud trail)"],"labels":["@aws-cdk/aws-cloudtrail"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/aws-cloudwatch)","(aws-cloudwatch)","(cloudwatch)","(cloud watch)"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-cloudwatch-actions)","(aws-cloudwatch-actions)","(cloudwatch-actions)","(cloudwatch actions)"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["rix0rrr"]}, + {"keywords":["(@aws-cdk/aws-cloudwatch)","(aws-cloudwatch)","(cloudwatch)","(cloud watch)"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["NetaNir"]}, + {"keywords":["(@aws-cdk/aws-cloudwatch-actions)","(aws-cloudwatch-actions)","(cloudwatch-actions)","(cloudwatch actions)"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["NetaNir"]}, {"keywords":["(@aws-cdk/aws-codeartifact)","(aws-codeartifact)","(codeartifact)","(code artifact)","(code-artifact)"],"labels":["@aws-cdk/aws-codeartifact"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-codebuild)","(aws-codebuild)","(codebuild)","(code build)","(code-build)"],"labels":["@aws-cdk/aws-codebuild"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-codecommit)","(aws-codecommit)","(codecommit)","(code commit)", "(code-commit)"],"labels":["@aws-cdk/aws-codecommit"],"assignees":["skinny85"]}, @@ -76,8 +76,8 @@ jobs: {"keywords":["(@aws-cdk/aws-dlm)","(aws-dlm)","(dlm)"],"labels":["@aws-cdk/aws-dlm"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-dms)","(aws-dms)","(dms)"],"labels":["@aws-cdk/aws-dms"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-docdb)","(aws-docdb)","(docdb)","(doc db)","(doc-db)"],"labels":["@aws-cdk/aws-docdb"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-dynamodb)","(aws-dynamodb)","(dynamodb)","(dynamo db)","(dynamo-db)"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-dynamodb-global)","(aws-dynamodb-global)","(dynamodb-global)","(dynamodb global)"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-dynamodb)","(aws-dynamodb)","(dynamodb)","(dynamo db)","(dynamo-db)"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["RomainMuller"]}, + {"keywords":["(@aws-cdk/aws-dynamodb-global)","(aws-dynamodb-global)","(dynamodb-global)","(dynamodb global)"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["RomainMuller"]}, {"keywords":["(@aws-cdk/aws-ec2)","(aws-ec2)","(ec2)","(vpc)"],"labels":["@aws-cdk/aws-ec2"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-ecr)","(aws-ecr)","(ecr)"],"labels":["@aws-cdk/aws-ecr"],"assignees":["MrArnoldPalmer"]}, {"keywords":["(@aws-cdk/aws-ecr-assets)","(aws-ecr-assets)","(ecr-assets)","(ecr assets)","(ecrassets)"],"labels":["@aws-cdk/aws-ecr-assets"],"assignees":["eladb"]}, 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 ce9e72a3d6855..fad4611dfad6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,20 +2,101 @@ 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.91.0](https://github.com/aws/aws-cdk/compare/v1.90.1...v1.91.0) (2021-02-23) +## [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) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **aws-appsync:** RdsDataSource now takes a ServerlessCluster instead of a DatabaseCluster +* **aws-appsync:** graphqlapi.addRdsDataSource now takes databaseName as its fourth argument + +### Features + +* **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) +* **cfnspec:** cloudformation spec v28.0.0 ([#13101](https://github.com/aws/aws-cdk/issues/13101)) ([13c9859](https://github.com/aws/aws-cdk/commit/13c9859cc62b3d472ba1be84b12d478f61f02ec9)) +* **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) * **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) @@ -102,7 +183,7 @@ All notable changes to this project will be documented in this file. See [standa ### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES -* **appmesh:** the properties virtualRouter and virtualNode of VirtualServiceProps have been replaced with the union-like class VirtualServiceProvider +* **appmesh:** the properties virtualRouter and virtualNode of VirtualServiceProps have been replaced with the union-like class VirtualServiceProvider * **appmesh**: the method `addVirtualService` has been removed from `IMesh` * **cloudfront:** experimental EdgeFunction stack names have changed from 'edge-lambda-stack-${region}' to 'edge-lambda-stack-${stackid}' to support multiple independent CloudFront distributions with EdgeFunctions. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b1a950a551fe..7615d7b10db4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,7 @@ and let us know if it's not up-to-date (even better, submit a PR with your corr - [Step 4: Commit](#step-4-commit) - [Step 5: Pull Request](#step-5-pull-request) - [Step 6: Merge](#step-6-merge) +- [Breaking Changes](#breaking-changes) - [Tools](#tools) - [Main build scripts](#main-build-scripts) - [Partial build tools](#partial-build-tools) @@ -266,6 +267,143 @@ BREAKING CHANGE: Description of what broke and how to achieve this behavior now * Once approved and tested, a maintainer will squash-merge to master and will use your PR title/description as the commit message. +## Breaking Changes + +Whenever you are making changes, there is a chance for those changes to be +*breaking* existing users of the library. A change is breaking if there are +programs that customers could have been writing against the current version +of the CDK, that will no longer "work correctly" with the proposed new +version of the CDK. + +Breaking changes are not allowed in *stable* libraries¹. They are permissible +but still *highly discouraged* in experimental libraries, and require explicit +callouts in the bodies of Pull Requests that introduce them. + +> ¹) Note that starting in version 2 of the CDK, the majority of library code will be +> bundled into a single main CDK library which will be considered stable, and so +> no code in there can undergo breaking changes. + +Breaking changes come in two flavors: + +* API surface changes +* Behavior changes + +### API surface changes + +This encompasses any changes that affect the shape of the API. Changes that +will make existing programs fail to compile are not allowed. Typical examples +of that are: + +* Renaming classes or methods +* Adding required properties to a struct that is used as an input to a constructor + or method. This also includes changing a type from nullable to non-nullable. +* Removing properties from a struct that is returned from a method, or removing + properties from a class. This also includes changing a type from non-nullable + to nullable. + +To see why the latter is a problem, consider the following class: + +```ts +class SomeClass { + public readonly count: number; + // ❓ let's say I want to change this to 'count?: number', + // i.e. make it optional. +} + +// Someone could have written the following code: +const obj = new SomeClass(); +console.log(obj.count + 1); + +// After the proposed change, this code that used to compile fine will now throw: +console.log(obj.count + 1); +// ~~~~~~~~~ Error: Object is possibly 'undefined'. +``` + +CDK comes with build tooling to check whether changes you made introduce breaking +changes to the API surface. In a package directory, run: + +```shell +$ yarn build +$ yarn compat +``` + +To figure out if the changes you made were breaking. See the section [API Compatibility +Checks](#api-compatibility-checks) for more information. + +#### Dealing with breaking API surface changes + +If you need to change the type of some API element, introduce a new API +element and mark the old API element as `@deprecated`. + +If you need to pretend to have a value for the purposes of implementing an API +and you don't actually have a useful value to return, it is acceptable to make +the property a `getter` and throw an exception (keeping in mind to write error +messages that will be useful to a user of your construct): + +```ts +class SomeClass implements ICountable { + constructor(private readonly _count?: number) { + } + + public get count(): number { + if (this._count === undefined) { + // ✅ DO: throw a descriptive error that tells the user what to do + throw new Error('This operation requires that a \'count\' is specified when SomeClass is created.'); + // ❌ DO NOT: just throw an error like 'count is missing' + } + return this._count; + } +} +``` + +### Behavior changes + +These are changes that do not directly affect the compilation of programs +written against the previous API, but may change their meaning. In practice, +even though the user didn't change their code, the CloudFormation template +that gets synthesized is now different. + +**Not all template changes are breaking changes!** Consider a user that has +created a Stack using the previous version of the library, has updated their +version of the CDK library and is now deploying an update. A behavior change +is breaking if: + +* The update cannot be applied at all +* The update can be applied but causes service interruption or data loss. + +Data loss happens when the [Logical +ID](https://docs.aws.amazon.com/cdk/latest/guide/identifiers.html#identifiers_logical_ids) +of a stateful resource changes, or one of the [resource properties that requires +replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html) +is modified. In both of these cases, CloudFormation will delete the +resource, and if it was a stateful resource like a database the data in it is now gone. + +If a change applies cleanly and does not cause any service interruption, it +is not breaking. Nevertheless, it might still be wise to avoid those kinds of +changes as users are understandably wary of unexpected template changes, will +scrutinize them heavily, and we don't want to cause unnecessary panic and churn +in our use base. + +Determining whether or not behavioral changes are breaking requires expertise +and judgement on the part of the library owner, and testing. + +#### Dealing with breaking behavior changes + +Most of the time, behavioral changes will arise because we want to change the +default value or default behavior of some property (i.e., we want to change the +interpretation of what it means if the value is missing). + +If the new behavior is going to be breaking, the user must opt in to it, either by: + +* Adding a new API element (class, property, method, ...) to have users + explicitly opt in to the new behavior at the source code level (potentially + `@deprecate`ing the old API element); or +* Use the [feature flag](#feature-flags) mechanism to have the user opt in to the new + behavior without changing the source code. + +Of these two, the first one is preferred if possible (as feature flags have +non-local effects which can cause unintended effects). + ## Tools The CDK is a big project, and at the moment, all of the CDK modules are mastered in a single monolithic repository diff --git a/DESIGN_GUIDELINES.md b/DESIGN_GUIDELINES.md index e6cebe9f434c6..0a8566ab9d90c 100644 --- a/DESIGN_GUIDELINES.md +++ b/DESIGN_GUIDELINES.md @@ -8,13 +8,77 @@ The purpose of this document is to provide guidelines for designing the APIs in the AWS Construct Library in order to ensure a consistent and integrated experience across the entire AWS surface area. +* [Preface](#preface) +* [What's Included](#what-s-included) +* [API Design](#api-design) + * [Modules](#modules) + * [Construct Class](#construct-class) + * [Construct Interface](#construct-interface) + * [Owned vs. Unowned Constructs](#owned-vs-unowned-constructs) + * [Abstract Base](#abstract-base) + * [Props](#props) + * [Types](#types) + * [Defaults](#defaults) + * [Flat](#flat) + * [Concise](#concise) + * [Naming](#naming) + * [Property Documentation](#property-documentation) + * [Enums](#enums) + * [Unions](#unions) + * [Attributes](#attributes) + * [Configuration](#configuration) + * [Prefer Additions](#prefer-additions) + * [Dropped Mutations](#dropped-mutations) + * [Factories](#factories) + * [Imports](#imports) + * [“from” Methods](#-from--methods) + * [From-attributes](#from-attributes) + * [Roles](#roles) + * [Resource Policies](#resource-policies) + * [VPC](#vpc) + * [Grants](#grants) + * [Metrics](#metrics) + * [Events](#events) + * [Connections](#connections) + * [Integrations](#integrations) + * [State](#state) + * [Physical Names - TODO](#physical-names---todo) + * [Tags](#tags) + * [Secrets](#secrets) +* [Project Structure](#project-structure) + * [Code Organization](#code-organization) +* [Implementation](#implementation) + * [General Principles](#general-principles) + * [Construct IDs](#construct-ids) + * [Errors](#errors) + * [Avoid Errors If Possible](#avoid-errors-if-possible) + * [Error reporting mechanism](#error-reporting-mechanism) + * [Throwing exceptions](#throwing-exceptions) + * [Never Catch Exceptions](#never-catch-exceptions) + * [Attaching (lazy) Validators](#attaching--lazy--validators) + * [Attaching Errors/Warnings](#attaching-errors-warnings) + * [Error messages](#error-messages) + * [Tokens](#tokens) +* [Documentation](#documentation) + * [Inline Documentation](#inline-documentation) + * [Readme](#readme) +* [Testing](#testing) + * [Unit tests](#unit-tests) + * [Integration tests](#integration-tests) + * [Versioning](#versioning) +* [Naming & Style](#naming---style) + * [Naming Conventions](#naming-conventions) + * [Coding Style](#coding-style) + +## Preface + As much as possible, the guidelines in this document are enforced using the [**awslint** tool](https://www.npmjs.com/package/awslint) which reflects on the APIs and verifies that the APIs adhere to the guidelines. When a guideline is backed by a linter rule, the rule name will be referenced like this: _[awslint:resource-class-is-construct]_. -For the purpose of this document we will use "Foo" to denote the official name +For the purpose of this document, we will use "Foo" to denote the official name of the resource as defined in the AWS CloudFormation resource specification (i.e. "Bucket", "Queue", "Topic", etc). This notation allows deriving names from the official name. For example, `FooProps` would be `BucketProps`, `TopicProps`, @@ -56,6 +120,73 @@ allows the library to be used from all supported programming languages. jsii poses restrictions on language features that cannot be idiomatically represented in target languages. +## What's Included + +The AWS Construct Library, which is shipped as part of the AWS CDK constructs +representing AWS resources. + +The AWS Construct Library has multiple layers of constructs, beginning +with low-level constructs, which we call _CFN Resources_ (or L1, short for +"level 1") or CFN Resources (short for CloudFormation). These constructs +directly represent all resources available in AWS CloudFormation. CFN Resources +are periodically generated from the AWS CloudFormation Resource +Specification. They are named **Cfn**_Xyz_, where _Xyz_ is name of the +resource. For example, CfnBucket represents the AWS::S3::Bucket AWS +CloudFormation resource. When you use Cfn resources, you must explicitly +configure all resource properties, which requires a complete understanding of +the details of the underlying AWS CloudFormation resource model. + +The next level of constructs, L2, also represent AWS resources, but with a +higher-level, intent-based API. They provide similar functionality, but provide +the defaults, boilerplate, and glue logic you'd be writing yourself with a CFN +Resource construct. L2 constructs offer convenient defaults and reduce the need +to know all the details about the AWS resources they represent, while providing +convenience methods that make it simpler to work with the resource. For example, +the `s3.Bucket` class represents an Amazon S3 bucket with additional properties +and methods, such as `bucket.addLifeCycleRule()`, which adds a lifecycle rule to +the bucket. + +Examples of behaviors that an L2 commonly include: + +- Strongly-typed modeling of the underlying L1 properties +- Methods for integrating other AWS resources (e.g., adding an event notification to + an S3 bucket). +- Modeling of permissions and resource policies +- Modeling of metrics + +In addition to the above, some L2s may introduce more complex and +helpful functionality, either part of the original L2 itself, or as part of a +separate construct. The most common form of these L2s are integration constructs +that model interactions between different services (e.g., SNS publishing to SQS, +CodePipeline actions that trigger Lambda functions). + +The next level of abstraction present within the CDK are what we designate as +"L2.5s": a step above the L2s in terms of abstraction, but not quite at the +level of complete patterns or applications. These constructs still largely +focus on a single logical resource -- in constrast to "patterns" which combine +multiple resources -- but are customized for a specific common usage scenario of +an L2. Examples of L2.5s in the CDK are `aws-apigateway.LambdaRestApi`, +`aws-lambda-nodejs.NodeJsFunction`, `aws-rds.ServerlessCluster` and `eks.FargateCluster`. + +L2.5 constructs will be considered for inclusion in the CDK if they... + +- cover a common usage scenario that can be used by a significant portion of + the community; +- provide significant ease of use over the base L2 (via usage-specific defaults + convenience methods or improved strong-typing); +- simplify or enable another L2 within the CDK + +The CDK also currently includes some even higher-level constructs, which we call +patterns. These constructs often involve multiple kinds of resources and are +designed to help you complete common tasks in AWS or represent entire +applications. For example, the +`aws-ecs-patterns.ApplicationLoadBalancedFargateService` construct represents an +architecture that includes an AWS Fargate container cluster employing an +Application Load Balancer (ALB). These patterns are typically difficult to +design to be one-size-fits-all and are best suited to be published as separate +libraries, rather than included directly in the CDK. The patterns that currently +exist in the CDK will be removed in the next CDK major version (CDKv2). + ## API Design ### Modules @@ -98,8 +229,8 @@ or abstractions. However, you will notice that some sections explicitly call out guidelines that apply only to AWS resources (and in many cases enforced/implemented by the **Resource** base class). -AWS services are modeled around the concept of *resources*. Service normally -expose through their APIs one or more resources, which can be provisioned +AWS services are modeled around the concept of *resources*. Services normally +expose one or more resources through their APIs, which can be provisioned through the APIs control plane or through AWS CloudFormation. Every resource available in the AWS platform will have a corresponding resource @@ -333,14 +464,31 @@ from harnessing the full power of the resource, and customizing its behavior. alignment. The **@default** documentation tag must be included on all optional properties -of interfaces. Since there are cases where the default behavior is not a -specific value but rather depends on circumstances/context, the default -documentation tag must always begin with a “**-**" and then include a -description of the default behavior _[awslint:props-default-doc]_. +of interfaces. + +In cases where the default behavior can be described by a value (typically the +case for booleans and enums, sometimes for strings and numbers), the value immediately +follows the **@default** tag and should be a valid JavaScript value (as in: +`@default false`, or `@default "stringValue"`). + +In the majority of cases, the default behavior is not a specific value but +rather depends on circumstances/context. The default documentation tag must +begin with a “**-**" and then include a description of the default behavior +_[awslint:props-default-doc]_. This is specially true if the property +is a complex value or a reference to an object: don't write `@default +undefined`, describe the behavior that happens if the property is not +supplied. -For example: +Describe the default value or default behavior, even if it's not CDK that +controls the default. For example, if an absent value does not get rendered +into the template and it's ultimately the AWS *service* that determines the +default behavior, we still describe it in our documentation. + +Examples: ```ts +// ✅ DO - uses a '-' and describes the behavior + /** * External KMS key to use for bucket encryption. * @@ -350,6 +498,32 @@ For example: encryptionKey?: kms.IEncryptionKey; ``` +```ts +/** + * External KMS key to use for bucket encryption. + * + * @default undefined + * ❌ DO NOT - that the value is 'undefined' by default is implied. However, + * what will the *behavior* be if the value is left out? + */ +encryptionKey?: kms.IEncryptionKey; +``` + +```ts +/** + * Minimum capacity of the AutoScaling resource + * + * @default - no minimum capacity + * ❌ DO NOT - there most certainly is. It's probably 0 or 1. + * + * // OR + * @default - the minimum capacity is the default minimum capacity + * ❌ DO NOT - this is circular and useless to the reader. + * Describe what will actually happen. + */ +minCapacity?: number; +``` + #### Flat Do not introduce artificial nesting for props. It hinders discoverability and @@ -397,12 +571,12 @@ For example, prefer “readCapacity” versus “readCapacityUnits”. We prefer the terminology used by the official AWS service documentation over new terminology, even if you think it's not ideal. It helps users diagnose issues and map the mental model of the construct to the service APIs, -documentation and examples. For example don't be tempted to change SQS's +documentation and examples. For example, don't be tempted to change SQS's **dataKeyReusePeriod** with **keyRotation** because it will be hard for people to diagnose problems. They won't be able to just search for “sqs dataKeyReuse” and find topics on it. -> We can relax this guidelines when this is about generic terms (like +> We can relax this guideline when this is about generic terms (like `httpStatus` instead of `statusCode`). The important semantics to preserve are for *service features*: I wouldn't want to rename "lambda layers" to "lambda dependencies" just because it makes more sense because then users won't be @@ -697,8 +871,8 @@ _[awslint:from-signature]_: #### “from” Methods Resource constructs should export static “from” methods for importing unowned -resources given one more of its physical attributes such as ARN, name, etc. All -constructs should have at least one "fromXxx" method _[awslint:from-method]_: +resources given one or more of its physical attributes such as ARN, name, etc. All +constructs should have at least one `fromXxx` method _[awslint:from-method]_: ```ts static fromFooArn(scope: Construct, id: string, bucketArn: string): IFoo; @@ -870,7 +1044,7 @@ vpcSubnetSelection?: ec2.SubnetSelection; ### Grants -Grants are one of the most powerful concept in the AWS Construct Library. They +Grants are one of the most powerful concepts in the AWS Construct Library. They offer a higher level, intent-based, API for managing IAM permissions for AWS resources. @@ -974,7 +1148,7 @@ class Function extends Resource implements IFunction { ### Events -Many AWS resource emit events to the CloudWatch event bus. Such resources should +Many AWS resources emit events to the CloudWatch event bus. Such resources should have a set of “onXxx” methods available on their construct interface _[awslint:events-in-interface]_. @@ -1216,19 +1390,6 @@ for (const az of availabilityZones) { ### Errors -#### Input Validation - -Prefer to validate input as early as it is passed into your code (ctor, methods, -etc) and bail out by throwing an **Error** (no need to create subclasses of -Error since all errors in the CDK are unrecoverable): - -* All lowercase sentences (usually they are printed after “Error: \”) -* Include a descriptive message -* Include the value provided -* Include the expected/allowed values -* No need to include information that can be obtained from the stack trace -* No need to add a period at the end of error messages - #### Avoid Errors If Possible Always prefer to do the right thing for the user instead of raising an @@ -1237,18 +1398,79 @@ example, VPC has **enableDnsHostnames** and **enableDnsSupport**. DNS hostnames *require* DNS support, so only fail if the user enabled DNS hostnames but explicitly disabled DNS support. Otherwise, auto-enable DNS support for them. +#### Error reporting mechanism + +There are three mechanism you can use to report errors: + +* Eagerly throw an exception (fails synthesis) +* Attach a (lazy) validator to a construct (fails synthesis) +* Attach errors to a construct (succeeds synthesis, fails deployment) + +Between these, the first two fail synthesis, while the latter doesn't. Failing synthesis +means that no Cloud Assembly will be produced. + +The distinction becomes apparent when you consider multiple stacks in the same Cloud +Assembly: + +* If synthesis fails due to an error in *one* stack (either by throwing an exception + or by failing validation), the other stack can also not be deployed. +* In contrast, if you attach an error to a construct in one stack, that stack cannot + be deployed but the other one still can. + +Choose one of the first two methods if the failure is caused by a misuse of the API, +which the user should be alerted to and fix as quickly as possible. Choose attaching +an error to a construct if the failure is due to environmental factors outside the +direct use of the API surface (for example, lack of context provider lookup values). + +#### Throwing exceptions + +This should be the preferred error reporting method. + +Validate input as early as it is passed into your code (ctor, methods, +etc) and bail out by throwing an `Error`. No need to create subclasses of +Error since all errors in the CDK are unrecoverable. + +When validating inputs, don't forget to account for the fact that these +values may be `Token`s and not available for inspection at synthesis time. + +Example: + +```ts +if (!Token.isUnresolved(props.minCapacity) && props.minCapacity < 1) { + throw new Error(`'minCapacity' should be at least 1, got '${props.minCapacity}'`); +} +``` + #### Never Catch Exceptions -All CDK errors are unrecoverable. If a method wishes to signal a recoverable +All CDK errors are unrecoverable. If a method wishes to signal a recoverable error, this should be modeled in a return value and not through exceptions. -#### Post Validation +#### Attaching (lazy) Validators + +In the rare case where the integrity of your construct can only be checked +after the app has completed its initialization, call the +**this.node.addValidation()** method to add a validation object. This will +generally only be necessary if you want to produce an error when a certain +interaction with your construct did *not* happen (for example, a property +that should have been configured over the lifetime of the construct, wasn't): + +Always prefer early input validation over post-validation, as the necessity +of these should be rare. + +Example: -In the rare case where the integrity of your construct can only be checked right -before synthesis, override the **Construct.validate()** method and return -meaningful errors. Always prefer early input validation over post-validation. +```ts +this.node.addValidation({ + // 'validate' should return a string[] list of errors + validate: () => this.rules.length === 0 + ? ['At least one Rule must be added. Call \'addRule()\' to add Rules.'] + : [] + } +}); +``` -#### Attached Errors/Warnings +#### Attaching Errors/Warnings You can also “attach” an error or a warning to a construct via the **Annotations** class. These methods (e.g., `Annotations.of(construct).addWarning`) @@ -1256,7 +1478,46 @@ will attach CDK metadata to your construct, which will be displayed to the user by the toolchain when the stack is deployed. Errors will not allow deployment and warnings will only be displayed in -highlight (unless **--strict** mode is used). +highlight (unless `--strict` mode is used). + +```ts +if (!Token.isUnresolved(subnetIds) && subnetIds.length < 2) { + Annotations.of(this).addError(`Need at least 2 subnet ids, got: ${JSON.stringify(subnetIds)}`); +} +``` + +#### Error messages + +Think about error messages from the point of view of the end user of the CDK. +This is not necessarily someone who knows about the internals of your +construct library, so try to phrase the message in a way that would make +sense to them. + +For example, if a value the user supplied gets handed off between a number of +functions before finally being validated, phrase the message in terms of the +API the user interacted with, not in terms of the internal APIs. + +A good error message should include the following components: + +* What went wrong, in a way that makes sense to a top-level user +* An example of the incorrect value provided (if applicable) +* An example of the expected/allowed values (if applicable) +* The message should explain the (most likely) cause and change the user can + make to rectify the situation + +The message should be all lowercase and not end in a period, or contain +information that can be obtained from the stack trace. + +```ts +// ✅ DO - show the value you got and be specific about what the user should do +`supply at least one of minCapacity or maxCapacity, got ${JSON.stringify(action)}` + +// ❌ DO NOT - this tells the user nothing about what's wrong or what they should do +`required values are missing` + +// ❌ DO NOT - this error only makes sense if you know the implementation +`'undefined' is not a number` +``` ### Tokens diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index 964bb4d5c7712..2ca2ca5b6067f 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -56,10 +56,3 @@ incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition.addVolume # We made properties optional and it's really fine but our differ doesn't think so. weakened:@aws-cdk/cloud-assembly-schema.DockerImageSource weakened:@aws-cdk/cloud-assembly-schema.FileSource - -# https://github.com/aws/aws-cdk/pull/13145 -removed:@aws-cdk/core.AssetStaging.isArchive -removed:@aws-cdk/core.AssetStaging.packaging -removed:@aws-cdk/core.BundlingOutput -removed:@aws-cdk/core.BundlingOptions.outputType - 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 a986d658240f5..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.21.0", - "jsii-pacmak": "^1.21.0", - "jsii-rosetta": "^1.21.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/test/integ.all-service-addons.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json index f9d42c3927b60..7ef708ccc31aa 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 @@ -3491,4 +3491,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json index 6b7a3f0e42086..0f176c27b6ab7 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json @@ -1111,56 +1111,36 @@ ] }, "Create": { - "service": "SQS", - "action": "sendMessage", - "parameters": { - "QueueUrl": { - "Ref": "nameserviceTaskRecordManagerEventsQueueF805A6C1" - }, - "DelaySeconds": 10, - "MessageBody": "{ \"prime\": true }", - "MessageAttributes": { - "HostedZoneId": { - "DataType": "String", - "StringValue": { - "Ref": "zoneEB40FF1E" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"SQS\",\"action\":\"sendMessage\",\"parameters\":{\"QueueUrl\":\"", + { + "Ref": "nameserviceTaskRecordManagerEventsQueueF805A6C1" }, - "RecordName": { - "DataType": "String", - "StringValue": "test-record" - } - } - }, - "physicalResourceId": { - "responsePath": "MessageId" - } + "\",\"DelaySeconds\":10,\"MessageBody\":\"{ \\\"prime\\\": true }\",\"MessageAttributes\":{\"HostedZoneId\":{\"DataType\":\"String\",\"StringValue\":\"", + { + "Ref": "zoneEB40FF1E" + }, + "\"},\"RecordName\":{\"DataType\":\"String\",\"StringValue\":\"test-record\"}}},\"physicalResourceId\":{\"responsePath\":\"MessageId\"}}" + ] + ] }, "Update": { - "service": "SQS", - "action": "sendMessage", - "parameters": { - "QueueUrl": { - "Ref": "nameserviceTaskRecordManagerEventsQueueF805A6C1" - }, - "DelaySeconds": 10, - "MessageBody": "{ \"prime\": true }", - "MessageAttributes": { - "HostedZoneId": { - "DataType": "String", - "StringValue": { - "Ref": "zoneEB40FF1E" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"SQS\",\"action\":\"sendMessage\",\"parameters\":{\"QueueUrl\":\"", + { + "Ref": "nameserviceTaskRecordManagerEventsQueueF805A6C1" }, - "RecordName": { - "DataType": "String", - "StringValue": "test-record" - } - } - }, - "physicalResourceId": { - "responsePath": "MessageId" - } + "\",\"DelaySeconds\":10,\"MessageBody\":\"{ \\\"prime\\\": true }\",\"MessageAttributes\":{\"HostedZoneId\":{\"DataType\":\"String\",\"StringValue\":\"", + { + "Ref": "zoneEB40FF1E" + }, + "\"},\"RecordName\":{\"DataType\":\"String\",\"StringValue\":\"test-record\"}}},\"physicalResourceId\":{\"responsePath\":\"MessageId\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -1210,7 +1190,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904" }, "S3Key": { "Fn::Join": [ @@ -1223,7 +1203,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -1236,7 +1216,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -1286,17 +1266,17 @@ "Type": "String", "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904": { "Type": "String", - "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 bucket for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF": { "Type": "String", - "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 key for asset version \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343ArtifactHash0C561FF5": { "Type": "String", - "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "Artifact hash for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" } }, "Outputs": { 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 1d41a6913794e..2aab9da2612fa 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 @@ -2235,4 +2235,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/assert/package.json b/packages/@aws-cdk/assert/package.json index 1416fd7f4673c..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.1" + "ts-jest": "^26.5.3" }, "dependencies": { "@aws-cdk/cloud-assembly-schema": "0.0.0", diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index 4d4d1a79eb797..926f45daf1436 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -157,13 +157,24 @@ book.addMethod('GET', getBookIntegration, { }); ``` +It is possible to also integrate with AWS services in a different region. The following code integrates with Amazon SQS in the +`eu-west-1` region. + +```ts +const getMessageIntegration = new apigateway.AwsIntegration({ + service: 'sqs', + path: 'queueName', + region: 'eu-west-1' +}); +``` + ## API Keys The following example shows how to use an API Key with a usage plan: ```ts const hello = new lambda.Function(this, 'hello', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'hello.handler', code: lambda.Code.fromAsset('lambda') }); @@ -230,7 +241,7 @@ The following example shows how to use a rate limited api key : ```ts const hello = new lambda.Function(this, 'hello', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'hello.handler', code: lambda.Code.fromAsset('lambda') }); @@ -260,7 +271,7 @@ have to define your models and mappings for the request, response, and integrati ```ts const hello = new lambda.Function(this, 'hello', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'hello.handler', code: lambda.Code.fromAsset('lambda') }); diff --git a/packages/@aws-cdk/aws-apigateway/lib/domain-name.ts b/packages/@aws-cdk/aws-apigateway/lib/domain-name.ts index 0437b986fdc74..c9f082121bff6 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/domain-name.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/domain-name.ts @@ -12,8 +12,9 @@ import { EndpointType, IRestApi } from './restapi'; export enum SecurityPolicy { /** Cipher suite TLS 1.0 */ TLS_1_0 = 'TLS_1_0', + /** Cipher suite TLS 1.2 */ - TLS_1_2 = 'TLS_1_2' + TLS_1_2 = 'TLS_1_2', } export interface DomainNameOptions { @@ -40,13 +41,13 @@ export interface DomainNameOptions { * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-domainname.html * @default SecurityPolicy.TLS_1_0 */ - readonly securityPolicy?: SecurityPolicy + readonly securityPolicy?: SecurityPolicy; /** * The mutual TLS authentication configuration for a custom domain name. * @default - mTLS is not configured. */ - readonly mtls?: MTLSConfig + readonly mtls?: MTLSConfig; } export interface DomainNameProps extends DomainNameOptions { @@ -83,7 +84,6 @@ export interface IDomainName extends IResource { * @attribute DistributionHostedZoneId,RegionalHostedZoneId */ readonly domainNameAliasHostedZoneId: string; - } export class DomainName extends Resource implements IDomainName { @@ -112,9 +112,9 @@ export class DomainName extends Resource implements IDomainName { const edge = endpointType === EndpointType.EDGE; if (!Token.isUnresolved(props.domainName) && /[A-Z]/.test(props.domainName)) { - throw new Error('domainName does not support uppercase letters. ' + - `got: '${props.domainName}'`); + throw new Error(`Domain name does not support uppercase letters. Got: ${props.domainName}`); } + const mtlsConfig = this.configureMTLS(props.mtls); const resource = new CfnDomainName(this, 'Resource', { domainName: props.domainName, @@ -176,10 +176,9 @@ export interface DomainNameAttributes { readonly domainNameAliasTarget: string; /** - * Thje Route53 hosted zone ID to use in order to connect a record set to this domain through an alias. + * The Route53 hosted zone ID to use in order to connect a record set to this domain through an alias. */ readonly domainNameAliasHostedZoneId: string; - } /** @@ -190,8 +189,9 @@ export interface MTLSConfig { * The bucket that the trust store is hosted in. */ readonly bucket: IBucket; + /** - * The key in S3 to look at for the trust store + * The key in S3 to look at for the trust store. */ readonly key: string; diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts index 6374bb404902a..0e607e25a21cf 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/aws.ts @@ -60,6 +60,13 @@ export interface AwsIntegrationProps { * Integration options, such as content handling, request/response mapping, etc. */ readonly options?: IntegrationOptions + + /** + * The region of the integrated AWS service. + * + * @default - same region as the stack + */ + readonly region?: string; } /** @@ -87,6 +94,7 @@ export class AwsIntegration extends Integration { resource: apiType, sep: '/', resourceName: apiValue, + region: props.region, }); }, }), diff --git a/packages/@aws-cdk/aws-apigateway/test/domains.test.ts b/packages/@aws-cdk/aws-apigateway/test/domains.test.ts index ee70b4e9cb98e..2b7f49f9a3977 100644 --- a/packages/@aws-cdk/aws-apigateway/test/domains.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/domains.test.ts @@ -1,5 +1,5 @@ -import '@aws-cdk/assert/jest'; import { ABSENT } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as acm from '@aws-cdk/aws-certificatemanager'; import { Bucket } from '@aws-cdk/aws-s3'; import { Stack } from '@aws-cdk/core'; @@ -43,8 +43,6 @@ describe('domains', () => { expect(stack.resolve(regionalDomain.domainNameAliasHostedZoneId)).toEqual({ 'Fn::GetAtt': ['mydomain592C948B', 'RegionalHostedZoneId'] }); expect(stack.resolve(edgeDomain.domainNameAliasDomainName)).toEqual({ 'Fn::GetAtt': ['yourdomain5FE30C81', 'DistributionDomainName'] }); expect(stack.resolve(edgeDomain.domainNameAliasHostedZoneId)).toEqual({ 'Fn::GetAtt': ['yourdomain5FE30C81', 'DistributionHostedZoneId'] }); - - }); test('default endpoint type is REGIONAL', () => { @@ -64,7 +62,6 @@ describe('domains', () => { 'EndpointConfiguration': { 'Types': ['REGIONAL'] }, 'RegionalCertificateArn': { 'Ref': 'Cert5C9FAEC1' }, }); - }); test('accepts different security policies', () => { @@ -111,7 +108,6 @@ describe('domains', () => { 'RegionalCertificateArn': { 'Ref': 'Cert5C9FAEC1' }, 'SecurityPolicy': ABSENT, }); - }); test('"mapping" can be used to automatically map this domain to the deployment stage of an API', () => { @@ -140,7 +136,6 @@ describe('domains', () => { 'Ref': 'apiDeploymentStageprod896C8101', }, }); - }); test('"addBasePathMapping" can be used to add base path mapping to the domain', () => { @@ -186,7 +181,6 @@ describe('domains', () => { 'Ref': 'api2DeploymentStageprod4120D74E', }, }); - }); test('a domain name can be defined with the API', () => { @@ -225,8 +219,6 @@ describe('domains', () => { 'Ref': 'apiDeploymentStageprod896C8101', }, }); - - }); test('a domain name can be added later', () => { @@ -265,8 +257,6 @@ describe('domains', () => { 'Ref': 'apiDeploymentStageprod896C8101', }, }); - - }); test('domain name cannot contain uppercase letters', () => { @@ -274,13 +264,10 @@ describe('domains', () => { const stack = new Stack(); const certificate = new acm.Certificate(stack, 'cert', { domainName: 'someDomainWithUpercase.domain.com' }); - // WHEN + // WHEN & THEN expect(() => { new apigw.DomainName(stack, 'someDomain', { domainName: 'someDomainWithUpercase.domain.com', certificate }); }).toThrow(/uppercase/); - - // THEN - }); test('multiple domain names can be added', () => { @@ -345,8 +332,6 @@ describe('domains', () => { 'Ref': 'apiDeploymentStageprod896C8101', }, }); - - }); test('"addBasePathMapping" can be used to add base path mapping to the domain with specific stage', () => { @@ -440,7 +425,6 @@ describe('domains', () => { 'RegionalCertificateArn': 'arn:aws:acm:us-east-1:1111111:certificate/11-3336f1-44483d-adc7-9cd375c5169d', 'MutualTlsAuthentication': { 'TruststoreUri': 's3://exampleBucket/someca.pem', 'TruststoreVersion': 'version' }, }); - }); test('base path mapping configures stage for RestApi creation', () => { @@ -466,8 +450,6 @@ describe('domains', () => { 'Ref': 'restApiWithStageDeploymentStageprodC82A6648', }, }); - - }); test('base path mapping configures stage for SpecRestApi creation', () => { diff --git a/packages/@aws-cdk/aws-apigateway/test/method.test.ts b/packages/@aws-cdk/aws-apigateway/test/method.test.ts index 36610126f3ec6..3fb5c580bc541 100644 --- a/packages/@aws-cdk/aws-apigateway/test/method.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/method.test.ts @@ -90,6 +90,35 @@ describe('method', () => { }); + test('integration can be set for a service in the provided region', () => { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigw.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false }); + + // WHEN + new apigw.Method(stack, 'my-method', { + httpMethod: 'POST', + resource: api.root, + integration: new apigw.AwsIntegration({ service: 'sqs', path: 'queueName', region: 'eu-west-1' }), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGateway::Method', { + Integration: { + IntegrationHttpMethod: 'POST', + Type: 'AWS', + Uri: { + 'Fn::Join': [ + '', + [ + 'arn:', { Ref: 'AWS::Partition' }, ':apigateway:eu-west-1:sqs:path/queueName', + ], + ], + }, + }, + }); + }); + test('integration with a custom http method can be set via a property', () => { // GIVEN const stack = new cdk.Stack(); 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-certificatemanager/README.md b/packages/@aws-cdk/aws-certificatemanager/README.md index 5c27597f6701b..320f22c91663d 100644 --- a/packages/@aws-cdk/aws-certificatemanager/README.md +++ b/packages/@aws-cdk/aws-certificatemanager/README.md @@ -76,7 +76,7 @@ const cert = new acm.Certificate(this, 'Certificate', { domainName: 'test.example.com', subjectAlternativeNames: ['cool.example.com', 'test.example.net'], validation: acm.CertificateValidation.fromDnsMultiZone({ - 'text.example.com': exampleCom, + 'test.example.com': exampleCom, 'cool.example.com': exampleCom, 'test.example.net': exampleNet, }), 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 1e220d75e96a9..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 @@ -29,7 +29,7 @@ "devDependencies": { "aws-sdk": "^2.596.0", "aws-sdk-mock": "^5.1.0", - "eslint": "^7.20.0", + "eslint": "^7.21.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", @@ -37,7 +37,7 @@ "eslint-plugin-standard": "^4.1.0", "jest": "^26.6.3", "lambda-tester": "^3.6.0", - "nock": "^13.0.7", - "ts-jest": "^26.5.1" + "nock": "^13.0.10", + "ts-jest": "^26.5.3" } } diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.expected.json b/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.expected.json index 23c4884db164f..e5db15d48809b 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.expected.json +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.expected.json @@ -72,7 +72,8 @@ "Arn" ] }, - "Runtime": "nodejs12.x" + "Runtime": "nodejs12.x", + "Description": "veni vidi vici" }, "DependsOn": [ "CustomReflectCustomResourceProviderRoleB4B29AEC" diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts index 1743444c1ad57..bdd4ae31af241 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.core-custom-resources.ts @@ -23,7 +23,8 @@ class TestStack extends Stack { const serviceToken = CustomResourceProvider.getOrCreate(this, resourceType, { codeDirectory: `${__dirname}/core-custom-resource-provider-fixture`, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, + description: 'veni vidi vici', }); const cr = new CustomResource(this, 'MyResource', { diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index 34b0922913c4c..debc3b09d734f 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -282,7 +282,7 @@ The following shows a Lambda@Edge function added to the default behavior and tri ```ts const myFunc = new cloudfront.experimental.EdgeFunction(this, 'MyFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), }); @@ -310,7 +310,7 @@ If the stack is in `us-east-1`, a "normal" `lambda.Function` can be used instead ```ts const myFunc = new lambda.Function(this, 'MyFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), }); @@ -321,14 +321,14 @@ you can also set a specific stack ID for each Lambda@Edge. ```ts const myFunc1 = new cloudfront.experimental.EdgeFunction(this, 'MyFunction1', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler1')), stackId: 'edge-lambda-stack-id-1' }); const myFunc2 = new cloudfront.experimental.EdgeFunction(this, 'MyFunction2', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler2')), stackId: 'edge-lambda-stack-id-2' diff --git a/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts b/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts index 33b0e070d144e..b12a56fe67e80 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/experimental/edge-function.ts @@ -51,8 +51,6 @@ export class EdgeFunction extends Resource implements lambda.IVersion { public readonly role?: iam.IRole; public readonly version: string; - // functionStack needed for `addAlias`. - private readonly functionStack: Stack; private readonly _edgeFunction: lambda.Function; constructor(scope: Construct, id: string, props: EdgeFunctionProps) { @@ -60,11 +58,10 @@ export class EdgeFunction extends Resource implements lambda.IVersion { // Create a simple Function if we're already in us-east-1; otherwise create a cross-region stack. const regionIsUsEast1 = !Token.isUnresolved(this.stack.region) && this.stack.region === 'us-east-1'; - const { functionStack, edgeFunction, edgeArn } = regionIsUsEast1 + const { edgeFunction, edgeArn } = regionIsUsEast1 ? this.createInRegionFunction(props) : this.createCrossRegionFunction(id, props); - this.functionStack = functionStack; this.edgeArn = edgeArn; this.functionArn = edgeArn; @@ -89,7 +86,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion { } public addAlias(aliasName: string, options: lambda.AliasOptions = {}): lambda.Alias { - return new lambda.Alias(this.functionStack, `Alias${aliasName}`, { + return new lambda.Alias(this._edgeFunction, `Alias${aliasName}`, { aliasName, version: this._edgeFunction.currentVersion, ...options, @@ -146,7 +143,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion { const edgeFunction = new lambda.Function(this, 'Fn', props); addEdgeLambdaToRoleTrustStatement(edgeFunction.role!); - return { edgeFunction, edgeArn: edgeFunction.currentVersion.edgeArn, functionStack: this.stack }; + return { edgeFunction, edgeArn: edgeFunction.currentVersion.edgeArn }; } /** Create a support stack and function in us-east-1, and a SSM reader in-region */ @@ -166,7 +163,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion { const edgeArn = this.createCrossRegionArnReader(parameterNamePrefix, parameterName, edgeFunction); - return { edgeFunction, edgeArn, functionStack }; + return { edgeFunction, edgeArn }; } private createCrossRegionArnReader(parameterNamePrefix: string, parameterName: string, edgeFunction: lambda.Function): string { @@ -183,7 +180,7 @@ export class EdgeFunction extends Resource implements lambda.IVersion { const resourceType = 'Custom::CrossRegionStringParameterReader'; const serviceToken = CustomResourceProvider.getOrCreate(this, resourceType, { codeDirectory: path.join(__dirname, 'edge-function'), - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, policyStatements: [{ Effect: 'Allow', Resource: parameterArnPrefix, @@ -233,7 +230,6 @@ export class EdgeFunction extends Resource implements lambda.IVersion { interface FunctionConfig { readonly edgeFunction: lambda.Function; readonly edgeArn: string; - readonly functionStack: Stack; } function addEdgeLambdaToRoleTrustStatement(role: iam.IRole) { 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/experimental/edge-function.test.ts b/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts index 7a743056dfcb7..55b0c2f4aeaac 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/experimental/edge-function.test.ts @@ -217,6 +217,17 @@ test('addAlias() creates alias in function stack', () => { }); }); +test('mutliple aliases with the same name can be added to the same stack', () => { + const fn1 = new cloudfront.experimental.EdgeFunction(stack, 'MyFn1', defaultEdgeFunctionProps()); + const fn2 = new cloudfront.experimental.EdgeFunction(stack, 'MyFn2', defaultEdgeFunctionProps()); + fn1.addAlias('live'); + fn2.addAlias('live'); + + const fnStack = getFnStack(); + expect(fnStack).toCountResources('AWS::Lambda::Function', 2); + expect(fnStack).toCountResources('AWS::Lambda::Alias', 2); +}); + test('addPermission() creates permissions in function stack', () => { const fn = new cloudfront.experimental.EdgeFunction(stack, 'MyFn', defaultEdgeFunctionProps()); diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json index 412498ec96a3f..c251765b20980 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.expected.json @@ -159,36 +159,9 @@ "FunctionArn" ] } - } - ], - "TargetOriginId": "integdistributionlambdacrossregionDistOrigin167A054D5", - "ViewerProtocolPolicy": "allow-all" - }, - "Enabled": true, - "HttpVersion": "http2", - "IPV6Enabled": true, - "Origins": [ - { - "CustomOriginConfig": { - "OriginProtocolPolicy": "https-only" }, - "DomainName": "www.example.com", - "Id": "integdistributionlambdacrossregionDistOrigin167A054D5" - } - ] - } - } - }, - "Dist286EC08DF": { - "Type": "AWS::CloudFront::Distribution", - "Properties": { - "DistributionConfig": { - "DefaultCacheBehavior": { - "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", - "Compress": true, - "LambdaFunctionAssociations": [ { - "EventType": "origin-request", + "EventType": "origin-response", "LambdaFunctionARN": { "Fn::GetAtt": [ "Lambda2ArnReader5ACFBE1F", @@ -197,7 +170,7 @@ } } ], - "TargetOriginId": "integdistributionlambdacrossregionDist2Origin14F08376D", + "TargetOriginId": "integdistributionlambdacrossregionDistOrigin167A054D5", "ViewerProtocolPolicy": "allow-all" }, "Enabled": true, @@ -208,8 +181,8 @@ "CustomOriginConfig": { "OriginProtocolPolicy": "https-only" }, - "DomainName": "www.example2.com", - "Id": "integdistributionlambdacrossregionDist2Origin14F08376D" + "DomainName": "www.example.com", + "Id": "integdistributionlambdacrossregionDistOrigin167A054D5" } ] } @@ -277,13 +250,13 @@ "Code": { "ZipFile": "foo" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "LambdaServiceRoleA8ED4D3B", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -307,6 +280,21 @@ }, "Name": "EdgeFunctionArnLambda" } + }, + "LambdaAliaslive79C8A712": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "FunctionName": { + "Ref": "LambdaD247545B" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "LambdaCurrentVersionDF706F6A97fb843e9bd06fcd2bb15eeace80e13e", + "Version" + ] + }, + "Name": "live" + } } } }, @@ -356,13 +344,13 @@ "Code": { "ZipFile": "foo" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "Lambda2ServiceRole31A072E1", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -386,6 +374,21 @@ }, "Name": "EdgeFunctionArnLambda2" } + }, + "Lambda2Aliaslive77F6085F": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "FunctionName": { + "Ref": "Lambda217CFB423" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "Lambda2CurrentVersion72012B74b9eef8becb98501bc795baca3c6169c4", + "Version" + ] + }, + "Name": "live" + } } } } diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.ts b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.ts index df2c26aecfd26..b1aed3d79da21 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-lambda-cross-region.ts @@ -22,25 +22,23 @@ const lambdaFunction2 = new cloudfront.experimental.EdgeFunction(stack, 'Lambda2 stackId: `edge-lambda-stack-${region}-2`, }); +lambdaFunction.addAlias('live'); +lambdaFunction2.addAlias('live'); + new cloudfront.Distribution(stack, 'Dist', { defaultBehavior: { origin: new TestOrigin('www.example.com'), cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, - edgeLambdas: [{ - functionVersion: lambdaFunction.currentVersion, - eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, - }], - }, -}); - -new cloudfront.Distribution(stack, 'Dist2', { - defaultBehavior: { - origin: new TestOrigin('www.example2.com'), - cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED, - edgeLambdas: [{ - functionVersion: lambdaFunction2.currentVersion, - eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, - }], + edgeLambdas: [ + { + functionVersion: lambdaFunction.currentVersion, + eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, + }, + { + functionVersion: lambdaFunction2.currentVersion, + eventType: cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE, + }, + ], }, }); 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-cloudtrail/README.md b/packages/@aws-cdk/aws-cloudtrail/README.md index 2e4f38576b540..3deccd47545ea 100644 --- a/packages/@aws-cdk/aws-cloudtrail/README.md +++ b/packages/@aws-cdk/aws-cloudtrail/README.md @@ -175,7 +175,7 @@ configures logging of Lambda data events for a specific Function. ```ts const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); const amazingFunction = new lambda.Function(stack, 'AnAmazingFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: "hello.handler", code: lambda.Code.fromAsset("lambda"), }); 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 5c1daacb54dd3..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,21 +225,70 @@ 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, { - withStat(st) { - self.validateMetricStat(st, metric); - - return dropUndefined({ - dimensions: st.dimensions, - namespace: st.namespace, - metricName: st.metricName, - period: st.period?.toSeconds(), - statistic: renderIfSimpleStatistic(st.statistic), - extendedStatistic: renderIfExtendedStatistic(st.statistic), - unit: st.unitFilter, - }); + withStat(stat, conf) { + self.validateMetricStat(stat, metric); + + if (conf.renderingProperties?.label == undefined) { + return dropUndefined({ + dimensions: stat.dimensions, + namespace: stat.namespace, + metricName: stat.metricName, + period: stat.period?.toSeconds(), + statistic: renderIfSimpleStatistic(stat.statistic), + extendedStatistic: renderIfExtendedStatistic(stat.statistic), + unit: stat.unitFilter, + }); + } + + return { + metrics: [ + { + metricStat: { + metric: { + metricName: stat.metricName, + namespace: stat.namespace, + dimensions: stat.dimensions, + }, + period: stat.period.toSeconds(), + stat: stat.statistic, + unit: stat.unitFilter, + }, + id: stat.metricName, + label: conf.renderingProperties?.label, + returnData: true, + } as CfnAlarm.MetricDataQueryProperty, + ], + }; }, withExpression() { diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/private/rendering.ts b/packages/@aws-cdk/aws-cloudwatch/lib/private/rendering.ts index e8dcb15cabc84..8553d9ad5c486 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/private/rendering.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/private/rendering.ts @@ -55,6 +55,7 @@ function metricGraphJson(metric: IMetric, yAxis?: string, id?: string) { withExpression(expr) { options.expression = expr.expression; + if (expr.period && expr.period !== 300) { options.period = expr.period; } }, }); diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-with-label.expected.json b/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-with-label.expected.json new file mode 100644 index 0000000000000..9ab6e14f29a6e --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-with-label.expected.json @@ -0,0 +1,50 @@ +{ + "Resources": { + "Alarm1F9009D71": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "Metrics": [ + { + "Id": "Metric", + "Label": "Metric [AVG: ${AVG}]", + "MetricStat": { + "Metric": { + "MetricName": "Metric", + "Namespace": "CDK/Test" + }, + "Period": 300, + "Stat": "Average" + }, + "ReturnData": true + } + ], + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "Threshold": 100 + } + }, + "Alarm2A7122E13": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "Metrics": [ + { + "Id": "Metric", + "Label": "Metric [AVG: ${AVG}]", + "MetricStat": { + "Metric": { + "MetricName": "Metric", + "Namespace": "CDK/Test" + }, + "Period": 300, + "Stat": "Average" + }, + "ReturnData": true + } + ], + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 3, + "Threshold": 100 + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-with-label.ts b/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-with-label.ts new file mode 100644 index 0000000000000..85aa1976b51aa --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-with-label.ts @@ -0,0 +1,32 @@ +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { Alarm, Metric } from '../lib'; + +class AlarmWithLabelIntegrationTest extends Stack { + + constructor(scope: App, id: string, props?: StackProps) { + super(scope, id, props); + + const testMetric = new Metric({ + namespace: 'CDK/Test', + metricName: 'Metric', + label: 'Metric [AVG: ${AVG}]', + }); + + new Alarm(this, 'Alarm1', { + metric: testMetric, + threshold: 100, + evaluationPeriods: 3, + }); + + testMetric.createAlarm(this, 'Alarm2', { + threshold: 100, + evaluationPeriods: 3, + }); + } +} + +const app = new App(); + +new AlarmWithLabelIntegrationTest(app, 'AlarmWithLabelIntegrationTest'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.math-alarm-and-dashboard.expected.json b/packages/@aws-cdk/aws-cloudwatch/test/integ.math-alarm-and-dashboard.expected.json index 7de0e4290cd65..8e9b235bb2b65 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.math-alarm-and-dashboard.expected.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.math-alarm-and-dashboard.expected.json @@ -88,7 +88,7 @@ { "Ref": "AWS::Region" }, - "\",\"metrics\":[[{\"label\":\"Total Messages\",\"expression\":\"m1+m2\"}],[\"AWS/SQS\",\"ApproximateNumberOfMessagesVisible\",\"QueueName\",\"", + "\",\"metrics\":[[{\"label\":\"Total Messages\",\"expression\":\"m1+m2\",\"period\":60}],[\"AWS/SQS\",\"ApproximateNumberOfMessagesVisible\",\"QueueName\",\"", { "Fn::GetAtt": [ "queue", @@ -120,7 +120,7 @@ { "Ref": "AWS::Region" }, - "\",\"metrics\":[[{\"label\":\"Total Messages\",\"expression\":\"m1+m2\"}],[\"AWS/SQS\",\"ApproximateNumberOfMessagesVisible\",\"QueueName\",\"", + "\",\"metrics\":[[{\"label\":\"Total Messages\",\"expression\":\"m1+m2\",\"period\":60}],[\"AWS/SQS\",\"ApproximateNumberOfMessagesVisible\",\"QueueName\",\"", { "Fn::GetAtt": [ "queue", 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-cloudwatch/test/test.metric-math.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.metric-math.ts index e8288ce092b27..b5eecac9ec52d 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.metric-math.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.metric-math.ts @@ -219,6 +219,28 @@ export = { test.done(); }, + 'top level period in a MathExpression is respected in its metrics'(test: Test) { + const graph = new GraphWidget({ + left: [ + a, + new MathExpression({ + expression: 'a + b', + usingMetrics: { a, b }, + period: Duration.minutes(1), + }), + ], + }); + + // THEN + graphMetricsAre(test, graph, [ + ['Test', 'ACount'], + [{ label: 'a + b', expression: 'a + b', period: 60 }], + ['Test', 'ACount', { visible: false, id: 'a', period: 60 }], + ['Test', 'BCount', { visible: false, id: 'b', period: 60 }], + ]); + test.done(); + }, + 'MathExpression controls period of metrics transitively used in it'(test: Test) { // Same as the previous test, but recursively diff --git a/packages/@aws-cdk/aws-codebuild/lib/source.ts b/packages/@aws-cdk/aws-codebuild/lib/source.ts index 19161ef9b6172..a14026da98711 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/source.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/source.ts @@ -381,7 +381,7 @@ export class FilterGroup { * Create a new FilterGroup with an added condition: * the push that is the source of the event must affect a file that matches the given pattern. * Note that you can only use this method if this Group contains only the `PUSH` event action, - * and only for GitHub and GitHubEnterprise sources. + * and only for GitHub, Bitbucket and GitHubEnterprise sources. * * @param pattern a regular expression */ @@ -393,7 +393,7 @@ export class FilterGroup { * Create a new FilterGroup with an added condition: * the push that is the source of the event must not affect a file that matches the given pattern. * Note that you can only use this method if this Group contains only the `PUSH` event action, - * and only for GitHub and GitHubEnterprise sources. + * and only for GitHub, Bitbucket and GitHubEnterprise sources. * * @param pattern a regular expression */ @@ -788,11 +788,6 @@ class BitBucketSource extends ThirdPartyGitSource { throw new Error('BitBucket sources do not support the PULL_REQUEST_REOPENED webhook event action'); } - // they also don't support file path conditions - if (this.anyWebhookFilterContainsFilePathConditions()) { - throw new Error('BitBucket sources do not support file path conditions for webhook filters'); - } - const superConfig = super.bind(_scope, _project); return { sourceProperty: { @@ -809,12 +804,6 @@ class BitBucketSource extends ThirdPartyGitSource { return fg._actions.findIndex(a => a === EventAction.PULL_REQUEST_REOPENED) !== -1; }) !== -1; } - - private anyWebhookFilterContainsFilePathConditions() { - return this.webhookFilters.findIndex(fg => { - return fg._filters.findIndex(f => f.type === WebhookFilterTypes.FILE_PATH) !== -1; - }) !== -1; - } } function set2Array(set: Set): T[] { diff --git a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts index d6341771d71b7..806ec437c20c6 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts @@ -1801,19 +1801,17 @@ export = { test.done(); }, - 'BitBucket sources do not support file path conditions'(test: Test) { + 'BitBucket sources support file path conditions'(test: Test) { const stack = new cdk.Stack(); const filterGroup = codebuild.FilterGroup.inEventOf(codebuild.EventAction.PUSH).andFilePathIs('.*'); - test.throws(() => { - new codebuild.Project(stack, 'Project', { - source: codebuild.Source.bitBucket({ - owner: 'owner', - repo: 'repo', - webhookFilters: [filterGroup], - }), - }); - }, /BitBucket sources do not support file path conditions for webhook filters/); + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.bitBucket({ + owner: 'owner', + repo: 'repo', + webhookFilters: [filterGroup], + }), + }); test.done(); }, diff --git a/packages/@aws-cdk/aws-codedeploy/test/lambda/test.custom-deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/test/lambda/test.custom-deployment-config.ts index 61917637c19d9..fdd8ba0c56be6 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/lambda/test.custom-deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/lambda/test.custom-deployment-config.ts @@ -53,49 +53,9 @@ export = { 'Arn', ], }, - Create: { - action: 'createDeploymentConfig', - service: 'CodeDeploy', - parameters: { - computePlatform: 'Lambda', - deploymentConfigName: 'CustomConfig.LambdaCanary5Percent1Minutes', - trafficRoutingConfig: { - timeBasedCanary: { - canaryPercentage: '5', - canaryInterval: '1', - }, - type: 'TimeBasedCanary', - }, - }, - physicalResourceId: { - id: 'CustomConfig.LambdaCanary5Percent1Minutes', - }, - }, - Update: { - action: 'createDeploymentConfig', - service: 'CodeDeploy', - parameters: { - computePlatform: 'Lambda', - deploymentConfigName: 'CustomConfig.LambdaCanary5Percent1Minutes', - trafficRoutingConfig: { - timeBasedCanary: { - canaryPercentage: '5', - canaryInterval: '1', - }, - type: 'TimeBasedCanary', - }, - }, - physicalResourceId: { - id: 'CustomConfig.LambdaCanary5Percent1Minutes', - }, - }, - Delete: { - action: 'deleteDeploymentConfig', - service: 'CodeDeploy', - parameters: { - deploymentConfigName: 'CustomConfig.LambdaCanary5Percent1Minutes', - }, - }, + Create: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"CustomConfig.LambdaCanary5Percent1Minutes","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"CustomConfig.LambdaCanary5Percent1Minutes"}}', + Update: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"CustomConfig.LambdaCanary5Percent1Minutes","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"CustomConfig.LambdaCanary5Percent1Minutes"}}', + Delete: '{"service":"CodeDeploy","action":"deleteDeploymentConfig","parameters":{"deploymentConfigName":"CustomConfig.LambdaCanary5Percent1Minutes"}}', })); expect(stack).to(haveResource('AWS::IAM::Policy', { @@ -134,27 +94,9 @@ export = { // THEN expect(stack).to(haveResourceLike('Custom::AWS', { - Create: { - parameters: { - deploymentConfigName: 'MyDeploymentConfig', - }, - physicalResourceId: { - id: 'MyDeploymentConfig', - }, - }, - Update: { - parameters: { - deploymentConfigName: 'MyDeploymentConfig', - }, - physicalResourceId: { - id: 'MyDeploymentConfig', - }, - }, - Delete: { - parameters: { - deploymentConfigName: 'MyDeploymentConfig', - }, - }, + Create: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"MyDeploymentConfig"}}', + Update: '{"service":"CodeDeploy","action":"createDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig","computePlatform":"Lambda","trafficRoutingConfig":{"type":"TimeBasedCanary","timeBasedCanary":{"canaryInterval":"1","canaryPercentage":"5"}}},"physicalResourceId":{"id":"MyDeploymentConfig"}}', + Delete: '{"service":"CodeDeploy","action":"deleteDeploymentConfig","parameters":{"deploymentConfigName":"MyDeploymentConfig"}}', })); test.done(); }, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/.gitignore b/packages/@aws-cdk/aws-codepipeline-actions/.gitignore index 0f4bf01dd552c..9f6a9219fad75 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/.gitignore +++ b/packages/@aws-cdk/aws-codepipeline-actions/.gitignore @@ -16,4 +16,5 @@ nyc.config.js *.snk !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/.npmignore b/packages/@aws-cdk/aws-codepipeline-actions/.npmignore index a94c531529866..9e88226921c33 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/.npmignore +++ b/packages/@aws-cdk/aws-codepipeline-actions/.npmignore @@ -23,4 +23,5 @@ tsconfig.json # exclude cdk artifacts **/cdk.out junit.xml -test/ \ No newline at end of file +test/ +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index cdc6cf1028ce3..b5aa1f5b502be 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -629,7 +629,7 @@ const lambdaCode = lambda.Code.fromCfnParameters(); const func = new lambda.Function(lambdaStack, 'Lambda', { code: lambdaCode, handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, }); // used to make sure each CDK synthesis produces a different Version const version = func.addVersion('NewVersion'); @@ -921,7 +921,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; const lambdaInvokeAction = new codepipeline_actions.LambdaInvokeAction({ actionName: 'Lambda', lambda: new lambda.Function(this, 'Func', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromInline(` const AWS = require('aws-sdk'); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/jest.config.js b/packages/@aws-cdk/aws-codepipeline-actions/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index 76381cfdbbb47..62a7868ece518 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -68,12 +68,12 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@types/lodash": "^4.14.168", - "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "lodash": "^4.17.21", - "nodeunit": "^0.11.3", + "nodeunit-shim": "0.0.0", "pkglint": "0.0.0" }, "dependencies": { @@ -177,6 +177,7 @@ }, "maturity": "stable", "cdk-build": { + "jest": true, "env": { "AWSLINT_BASE_CONSTRUCT": true } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/bitbucket-source-action.test.ts similarity index 97% rename from packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/bitbucket-source-action.test.ts index a4f120f2abf68..942811b94b2fd 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/bitbucket-source-action.test.ts @@ -2,12 +2,12 @@ import { expect, haveResourceLike } from '@aws-cdk/assert'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -export = { +nodeunitShim({ 'BitBucket source Action': { 'produces the correct configuration when added to a pipeline'(test: Test) { const stack = new Stack(); @@ -82,7 +82,7 @@ export = { test.done(); }, -}; +}); function createBitBucketAndCodeBuildPipeline(stack: Stack, props: { codeBuildCloneOutput: boolean }): void { const sourceOutput = new codepipeline.Artifact(); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/cloudformation-pipeline-actions.test.ts similarity index 99% rename from packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/cloudformation-pipeline-actions.test.ts index 22bf46a15a641..28a22d970a1c0 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.cloudformation-pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/cloudformation-pipeline-actions.test.ts @@ -5,12 +5,12 @@ import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -export = { +nodeunitShim({ 'CreateChangeSetAction can be used to make a change set from a CodePipeline'(test: Test) { const stack = new cdk.Stack(); @@ -712,7 +712,7 @@ export = { test.done(); }, }, -}; +}); /** * A test stack with a half-prepared pipeline ready to add CloudFormation actions to diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/pipeline-actions.test.ts similarity index 97% rename from packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/pipeline-actions.test.ts index 66e23dcfe0584..1433ffc6e2860 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/test.pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/pipeline-actions.test.ts @@ -5,12 +5,12 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; import * as _ from 'lodash'; -import * as nodeunit from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; -export = nodeunit.testCase({ +nodeunitShim({ CreateReplaceChangeSet: { - 'works'(test: nodeunit.Test) { + 'works'(test: Test) { const app = new cdk.App(); const stack = new cdk.Stack(app, 'Stack'); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); @@ -51,7 +51,7 @@ export = nodeunit.testCase({ test.done(); }, - 'uses a single permission statement if the same ChangeSet name is used'(test: nodeunit.Test) { + 'uses a single permission statement if the same ChangeSet name is used'(test: Test) { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const artifact = new codepipeline.Artifact('TestArtifact'); @@ -110,7 +110,7 @@ export = nodeunit.testCase({ }, ExecuteChangeSet: { - 'works'(test: nodeunit.Test) { + 'works'(test: Test) { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const stage = new StageDouble({ @@ -137,7 +137,7 @@ export = nodeunit.testCase({ test.done(); }, - 'uses a single permission statement if the same ChangeSet name is used'(test: nodeunit.Test) { + 'uses a single permission statement if the same ChangeSet name is used'(test: Test) { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); new StageDouble({ @@ -181,7 +181,7 @@ export = nodeunit.testCase({ }, }, - 'the CreateUpdateStack Action sets the DescribeStack*, Create/Update/DeleteStack & PassRole permissions'(test: nodeunit.Test) { + 'the CreateUpdateStack Action sets the DescribeStack*, Create/Update/DeleteStack & PassRole permissions'(test: Test) { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const action = new cpactions.CloudFormationCreateUpdateStackAction({ @@ -207,7 +207,7 @@ export = nodeunit.testCase({ test.done(); }, - 'the DeleteStack Action sets the DescribeStack*, DeleteStack & PassRole permissions'(test: nodeunit.Test) { + 'the DeleteStack Action sets the DescribeStack*, DeleteStack & PassRole permissions'(test: Test) { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const action = new cpactions.CloudFormationDeleteStackAction({ @@ -238,7 +238,7 @@ interface PolicyStatementJson { } function _assertActionMatches( - test: nodeunit.Test, + test: Test, stack: cdk.Stack, actions: FullAction[], provider: string, @@ -279,7 +279,7 @@ function _hasAction( } function _assertPermissionGranted( - test: nodeunit.Test, + test: Test, stack: cdk.Stack, statements: iam.PolicyStatement[], action: string, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/test.codebuild-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts similarity index 99% rename from packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/test.codebuild-action.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts index 68e5acaa849ca..a451aa51dcc42 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/test.codebuild-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts @@ -5,12 +5,12 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; import { App, SecretValue, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -export = { +nodeunitShim({ 'CodeBuild action': { 'that is cross-account and has outputs': { 'causes an error'(test: Test) { @@ -337,4 +337,4 @@ export = { }, }, }, -}; +}); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/codecommit-source-action.test.ts similarity index 99% rename from packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/codecommit-source-action.test.ts index 3dc5b1c739d4e..c3e2f4b9b6582 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/codecommit-source-action.test.ts @@ -4,12 +4,12 @@ import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; import { Stack, Lazy } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -export = { +nodeunitShim({ 'CodeCommit Source Action': { 'by default does not poll for source changes and uses Events'(test: Test) { const stack = new Stack(); @@ -430,7 +430,7 @@ export = { test.done(); }, }, -}; +}); function minimalPipeline(stack: Stack, trigger: cpactions.CodeCommitTrigger | undefined): codepipeline.Pipeline { const sourceOutput = new codepipeline.Artifact(); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/test.ecs-deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/ecs-deploy-action.test.ts similarity index 99% rename from packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/test.ecs-deploy-action.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/ecs-deploy-action.test.ts index b75809ad8f515..4444ea9d409df 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/test.ecs-deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/ecs-deploy-action.test.ts @@ -2,10 +2,10 @@ import { expect, haveResourceLike } from '@aws-cdk/assert'; import * as codedeploy from '@aws-cdk/aws-codedeploy'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; -export = { +nodeunitShim({ 'CodeDeploy ECS Deploy Action': { 'throws an exception if more than 4 container image inputs are provided'(test: Test) { const stack = new cdk.Stack(); @@ -198,7 +198,7 @@ export = { test.done(); }, }, -}; +}); function addEcsDeploymentGroup(stack: cdk.Stack): codedeploy.IEcsDeploymentGroup { return codedeploy.EcsDeploymentGroup.fromEcsDeploymentGroupAttributes( diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/test.ecr-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/ecr-source-action.test.ts similarity index 96% rename from packages/@aws-cdk/aws-codepipeline-actions/test/ecr/test.ecr-source-action.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/ecr/ecr-source-action.test.ts index b27ad5f89f880..aacbb856e065c 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/test.ecr-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/ecr-source-action.test.ts @@ -3,12 +3,12 @@ import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ecr from '@aws-cdk/aws-ecr'; import { Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -export = { +nodeunitShim({ 'ECR source Action': { 'exposes variables for other actions to consume'(test: Test) { const stack = new Stack(); @@ -63,4 +63,4 @@ export = { test.done(); }, }, -}; +}); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/test.ecs-deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts similarity index 98% rename from packages/@aws-cdk/aws-codepipeline-actions/test/ecs/test.ecs-deploy-action.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts index 1343850871206..841ca3948ec83 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/test.ecs-deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts @@ -4,10 +4,10 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; -export = { +nodeunitShim({ 'ECS deploy Action': { 'throws an exception if neither inputArtifact nor imageFile were provided'(test: Test) { const service = anyEcsService(); @@ -198,7 +198,7 @@ export = { test.done(); }, }, -}; +}); function anyEcsService(): ecs.FargateService { const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/github/test.github-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/github/github-source-action.test.ts similarity index 98% rename from packages/@aws-cdk/aws-codepipeline-actions/test/github/test.github-source-action.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/github/github-source-action.test.ts index 56131d3d15b1e..a73726e055459 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/github/test.github-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/github/github-source-action.test.ts @@ -2,12 +2,12 @@ import { expect, haveResourceLike, SynthUtils } from '@aws-cdk/assert'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { SecretValue, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -export = { +nodeunitShim({ 'GitHub source Action': { 'exposes variables for other actions to consume'(test: Test) { const stack = new Stack(); @@ -208,4 +208,4 @@ export = { test.done(); }, }, -}; +}); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/test.lambda-invoke-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts similarity index 81% rename from packages/@aws-cdk/aws-codepipeline-actions/test/lambda/test.lambda-invoke-action.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts index 035d8ddb63a38..64eecdb801b1c 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/test.lambda-invoke-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/lambda/lambda-invoke-action.test.ts @@ -1,24 +1,27 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; -import { Aws, Lazy, SecretValue, Stack, Token } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { App, Aws, Lazy, SecretValue, Stack, Token } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -export = { - 'Lambda invoke Action': { - 'properly serializes the object passed in userParameters'(test: Test) { +const s3GrantWriteCtx = { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }; + +describe('', () => { + describe('Lambda invoke Action', () => { + test('properly serializes the object passed in userParameters', () => { const stack = stackIncludingLambdaInvokeCodePipeline({ userParams: { key: 1234, }, }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ {}, { @@ -31,19 +34,19 @@ export = { ], }, ], - })); + }); + - test.done(); - }, + }); - 'properly resolves any Tokens passed in userParameters'(test: Test) { + test('properly resolves any Tokens passed in userParameters', () => { const stack = stackIncludingLambdaInvokeCodePipeline({ userParams: { key: Lazy.string({ produce: () => Aws.REGION }), }, }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ {}, { @@ -67,19 +70,19 @@ export = { ], }, ], - })); + }); + - test.done(); - }, + }); - 'properly resolves any stringified Tokens passed in userParameters'(test: Test) { + test('properly resolves any stringified Tokens passed in userParameters', () => { const stack = stackIncludingLambdaInvokeCodePipeline({ userParams: { key: Token.asString(null), }, }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ {}, { @@ -92,17 +95,17 @@ export = { ], }, ], - })); + }); + - test.done(); - }, + }); - "assigns the Action's Role with read permissions to the Bucket if it has only inputs"(test: Test) { + test("assigns the Action's Role with read permissions to the Bucket if it has only inputs", () => { const stack = stackIncludingLambdaInvokeCodePipeline({ lambdaInput: new codepipeline.Artifact(), }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -131,18 +134,18 @@ export = { }, ], }, - })); + }); + - test.done(); - }, + }); - "assigns the Action's Role with write permissions to the Bucket if it has only outputs"(test: Test) { + testFutureBehavior("assigns the Action's Role with write permissions to the Bucket if it has only outputs", s3GrantWriteCtx, App, (app) => { const stack = stackIncludingLambdaInvokeCodePipeline({ lambdaOutput: new codepipeline.Artifact(), // no input to the Lambda Action - we want write permissions only in this case - }); + }, app); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -157,7 +160,7 @@ export = { { 'Action': [ 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], 'Effect': 'Allow', @@ -172,18 +175,18 @@ export = { }, ], }, - })); + }); + - test.done(); - }, + }); - "assigns the Action's Role with read-write permissions to the Bucket if it has both inputs and outputs"(test: Test) { + testFutureBehavior("assigns the Action's Role with read-write permissions to the Bucket if it has both inputs and outputs", s3GrantWriteCtx, App, (app) => { const stack = stackIncludingLambdaInvokeCodePipeline({ lambdaInput: new codepipeline.Artifact(), lambdaOutput: new codepipeline.Artifact(), - }); + }, app); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -213,7 +216,7 @@ export = { { 'Action': [ 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], 'Effect': 'Allow', @@ -228,12 +231,12 @@ export = { }, ], }, - })); + }); - test.done(); - }, - 'exposes variables for other actions to consume'(test: Test) { + }); + + test('exposes variables for other actions to consume', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -269,7 +272,7 @@ export = { ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -290,12 +293,12 @@ export = { ], }, ], - })); + }); - test.done(); - }, - }, -}; + + }); + }); +}); interface HelperProps { readonly userParams?: { [key: string]: any }; @@ -303,8 +306,8 @@ interface HelperProps { readonly lambdaOutput?: codepipeline.Artifact; } -function stackIncludingLambdaInvokeCodePipeline(props: HelperProps) { - const stack = new Stack(); +function stackIncludingLambdaInvokeCodePipeline(props: HelperProps, app?: App) { + const stack = new Stack(app); new codepipeline.Pipeline(stack, 'Pipeline', { stages: [ diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/test.manual-approval.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/manual-approval.test.ts similarity index 96% rename from packages/@aws-cdk/aws-codepipeline-actions/test/test.manual-approval.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/manual-approval.test.ts index cac35015eda15..121e87cd9eb4e 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/test.manual-approval.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/manual-approval.test.ts @@ -2,12 +2,12 @@ import { expect, haveResourceLike } from '@aws-cdk/assert'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as sns from '@aws-cdk/aws-sns'; import { SecretValue, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../lib'; /* eslint-disable quote-props */ -export = { +nodeunitShim({ 'manual approval Action': { 'allows passing an SNS Topic when constructing it'(test: Test) { const stack = new Stack(); @@ -75,4 +75,4 @@ export = { test.done(); }, }, -}; +}); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline.test.ts similarity index 99% rename from packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/pipeline.test.ts index 242e55b40e039..0c0491d778150 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline.test.ts @@ -8,12 +8,12 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; import { App, Aws, CfnParameter, ConstructNode, SecretValue, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../lib'; /* eslint-disable quote-props */ -export = { +nodeunitShim({ 'basic pipeline'(test: Test) { const stack = new Stack(); @@ -1126,4 +1126,4 @@ export = { test.done(); }, }, -}; +}); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/s3/test.s3-deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-deploy-action.test.ts similarity index 78% rename from packages/@aws-cdk/aws-codepipeline-actions/test/s3/test.s3-deploy-action.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-deploy-action.test.ts index 8184c61bc3fce..2e2b5cef98ace 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/s3/test.s3-deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-deploy-action.test.ts @@ -1,19 +1,20 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; import { App, Duration, SecretValue, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -export = { - 'S3 Deploy Action': { - 'by default extract artifacts'(test: Test) { +describe('', () => { + describe('S3 Deploy Action', () => { + test('by default extract artifacts', () => { const stack = new Stack(); minimalPipeline(stack); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -43,16 +44,16 @@ export = { ], }, ], - })); + }); - test.done(); - }, - 'grant the pipeline correct access to the target bucket'(test: Test) { - const stack = new Stack(); + }); + + testFutureBehavior('grant the pipeline correct access to the target bucket', { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }, App, (app) => { + const stack = new Stack(app); minimalPipeline(stack); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -62,7 +63,7 @@ export = { 's3:GetBucket*', 's3:List*', 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], }, @@ -73,18 +74,18 @@ export = { }, ], }, - })); + }); - test.done(); - }, - 'kebab-case CannedACL value'(test: Test) { + }); + + test('kebab-case CannedACL value', () => { const stack = new Stack(); minimalPipeline(stack, { accessControl: s3.BucketAccessControl.PUBLIC_READ_WRITE, }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ {}, { @@ -97,12 +98,12 @@ export = { ], }, ], - })); + }); + - test.done(); - }, + }); - 'allow customizing cache-control'(test: Test) { + test('allow customizing cache-control', () => { const stack = new Stack(); minimalPipeline(stack, { cacheControl: [ @@ -112,7 +113,7 @@ export = { ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ {}, { @@ -125,18 +126,18 @@ export = { ], }, ], - })); + }); + - test.done(); - }, + }); - 'allow customizing objectKey (deployment path on S3)'(test: Test) { + test('allow customizing objectKey (deployment path on S3)', () => { const stack = new Stack(); minimalPipeline(stack, { objectKey: '/a/b/c', }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ {}, { @@ -149,12 +150,12 @@ export = { ], }, ], - })); + }); + - test.done(); - }, + }); - 'correctly makes the action cross-region for a Bucket imported with a different region'(test: Test) { + test('correctly makes the action cross-region for a Bucket imported with a different region', () => { const app = new App(); const stack = new Stack(app, 'PipelineStack', { env: { account: '123456789012', region: 'us-west-2' }, @@ -168,7 +169,7 @@ export = { bucket: deployBucket, }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ {}, { @@ -181,12 +182,12 @@ export = { ], }, ], - })); + }); + - test.done(); - }, - }, -}; + }); + }); +}); interface MinimalPipelineOptions { readonly accessControl?: s3.BucketAccessControl; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/s3/test.s3-source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-source-action.test.ts similarity index 99% rename from packages/@aws-cdk/aws-codepipeline-actions/test/s3/test.s3-source-action.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-source-action.test.ts index fc069cffae6d3..3ae1210dcd8f7 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/s3/test.s3-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-source-action.test.ts @@ -3,12 +3,12 @@ import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; import { Lazy, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -export = { +nodeunitShim({ 'S3 Source Action': { 'by default polls for source changes and does not use Events'(test: Test) { const stack = new Stack(); @@ -269,7 +269,7 @@ export = { test.done(); }, }, -}; +}); interface MinimalPipelineOptions { readonly trigger?: cpactions.S3Trigger; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/test.servicecatalog-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/servicecatalog-action.test.ts similarity index 98% rename from packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/test.servicecatalog-action.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/servicecatalog-action.test.ts index 8c4101552c37f..1415b2a084eaf 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/test.servicecatalog-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/servicecatalog-action.test.ts @@ -2,12 +2,12 @@ import { expect, haveResourceLike } from '@aws-cdk/assert'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -export = { +nodeunitShim({ 'addAction succesfully leads to creation of codepipeline service catalog action with properly formatted TemplateFilePath'(test: Test) { // GIVEN const stack = new TestFixture(); @@ -98,7 +98,7 @@ export = { test.done(); }, -}; +}); /** * A test stack with a half-prepared pipeline ready to add CloudFormation actions to diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/stepfunctions/test.stepfunctions-invoke-actions.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/stepfunctions/stepfunctions-invoke-actions.test.ts similarity index 98% rename from packages/@aws-cdk/aws-codepipeline-actions/test/stepfunctions/test.stepfunctions-invoke-actions.ts rename to packages/@aws-cdk/aws-codepipeline-actions/test/stepfunctions/stepfunctions-invoke-actions.test.ts index af9bf28b90640..47e33e7d5b5d8 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/stepfunctions/test.stepfunctions-invoke-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/stepfunctions/stepfunctions-invoke-actions.test.ts @@ -3,10 +3,10 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; import * as stepfunction from '@aws-cdk/aws-stepfunctions'; import { Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; +import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; -export = { +nodeunitShim({ 'StepFunctions Invoke Action': { 'Verify stepfunction configuration properties are set to specific values'(test: Test) { const stack = new Stack(); @@ -144,7 +144,7 @@ export = { }, }, -}; +}); function minimalPipeline(stack: Stack): codepipeline.IStage { const sourceOutput = new codepipeline.Artifact(); diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 5067ae8f44b63..d3f90ff026b36 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -343,7 +343,7 @@ on the construct, as so - ```ts const authChallengeFn = new lambda.Function(this, 'authChallengeFn', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromInline('auth challenge'), }); @@ -357,7 +357,7 @@ const userpool = new cognito.UserPool(this, 'myuserpool', { }); userpool.addTrigger(cognito.UserPoolOperation.USER_MIGRATION, new lambda.Function(this, 'userMigrationFn', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromInline('user migration'), })); @@ -418,6 +418,7 @@ The following third-party identity providers are currently supported in the CDK - [Login With Amazon](https://developer.amazon.com/apps-and-games/login-with-amazon) - [Facebook Login](https://developers.facebook.com/docs/facebook-login/) - [Google Login](https://developers.google.com/identity/sign-in/web/sign-in) +- [Sign In With Apple](https://developer.apple.com/sign-in-with-apple/get-started/) The following code configures a user pool to federate with the third party provider, 'Login with Amazon'. The identity provider needs to be configured with a set of credentials that the Cognito backend can use to federate with the @@ -582,12 +583,12 @@ The default behaviour is to allow read and write permissions on all attributes. const pool = new cognito.UserPool(this, 'Pool'); const clientWriteAttributes = (new ClientAttributes()) - .withStandardAttributes({name: true, email: true}) - .withCustomAttributes(['favouritePizza']); + .withStandardAttributes({fullname: true, email: true}) + .withCustomAttributes('favouritePizza', 'favouriteBeverage'); const clientReadAttributes = clientWriteAttributes .withStandardAttributes({emailVerified: true}) - .withCustomAttributes(['pointsEarned']); + .withCustomAttributes('pointsEarned'); pool.addClient('app-client', { // ... diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts index ea5693f45d1c4..866c11015ecfd 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -157,6 +157,12 @@ export class OAuthScope { * Identity providers supported by the UserPoolClient */ export class UserPoolClientIdentityProvider { + /** + * Allow users to sign in using 'Sign In With Apple'. + * A `UserPoolIdentityProviderApple` must be attached to the user pool. + */ + public static readonly APPLE = new UserPoolClientIdentityProvider('SignInWithApple'); + /** * Allow users to sign in using 'Facebook Login'. * A `UserPoolIdentityProviderFacebook` must be attached to the user pool. diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/apple.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/apple.ts new file mode 100644 index 0000000000000..c1fbd6d4ca9fa --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/apple.ts @@ -0,0 +1,63 @@ +import { Construct } from 'constructs'; +import { CfnUserPoolIdentityProvider } from '../cognito.generated'; +import { UserPoolIdentityProviderProps } from './base'; +import { UserPoolIdentityProviderBase } from './private/user-pool-idp-base'; + +/** + * Properties to initialize UserPoolAppleIdentityProvider + */ +export interface UserPoolIdentityProviderAppleProps extends UserPoolIdentityProviderProps { + /** + * The client id recognized by Apple APIs. + * @see https://developer.apple.com/documentation/sign_in_with_apple/clientconfigi/3230948-clientid + */ + readonly clientId: string; + /** + * The teamId for Apple APIs to authenticate the client. + */ + readonly teamId: string; + /** + * The keyId (of the same key, which content has to be later supplied as `privateKey`) for Apple APIs to authenticate the client. + */ + readonly keyId: string; + /** + * The privateKey content for Apple APIs to authenticate the client. + */ + readonly privateKey: string; + /** + * The list of apple permissions to obtain for getting access to the apple profile + * @see https://developer.apple.com/documentation/sign_in_with_apple/clientconfigi/3230955-scope + * @default [ name ] + */ + readonly scopes?: string[]; +} + +/** + * Represents a identity provider that integrates with 'Apple' + * @resource AWS::Cognito::UserPoolIdentityProvider + */ +export class UserPoolIdentityProviderApple extends UserPoolIdentityProviderBase { + public readonly providerName: string; + + constructor(scope: Construct, id: string, props: UserPoolIdentityProviderAppleProps) { + super(scope, id, props); + + const scopes = props.scopes ?? ['name']; + + const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { + userPoolId: props.userPool.userPoolId, + providerName: 'SignInWithApple', // must be 'SignInWithApple' when the type is 'SignInWithApple' + providerType: 'SignInWithApple', + providerDetails: { + client_id: props.clientId, + team_id: props.teamId, + key_id: props.keyId, + private_key: props.privateKey, + authorize_scopes: scopes.join(' '), + }, + attributeMapping: super.configureAttributeMapping(), + }); + + this.providerName = super.getResourceNameAttribute(resource.ref); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts index be155fca69a6d..08278947b9e04 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts @@ -4,6 +4,15 @@ import { IUserPool } from '../user-pool'; * An attribute available from a third party identity provider. */ export class ProviderAttribute { + /** The email attribute provided by Apple */ + public static readonly APPLE_EMAIL = new ProviderAttribute('email'); + /** The name attribute provided by Apple */ + public static readonly APPLE_NAME = new ProviderAttribute('name'); + /** The first name attribute provided by Apple */ + public static readonly APPLE_FIRST_NAME = new ProviderAttribute('firstName'); + /** The last name attribute provided by Apple */ + public static readonly APPLE_LAST_NAME = new ProviderAttribute('lastName'); + /** The user id attribute provided by Amazon */ public static readonly AMAZON_USER_ID = new ProviderAttribute('user_id'); /** The email attribute provided by Amazon */ diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts index dbc63a9854f37..321ee0ecad5d9 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts @@ -1,4 +1,5 @@ export * from './base'; +export * from './apple'; export * from './amazon'; export * from './facebook'; export * from './google'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index c9bae0f953907..3a981167956a3 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -110,7 +110,8 @@ "props-physical-name:@aws-cdk/aws-cognito.UserPoolDomainProps", "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderFacebookProps", "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAmazonProps", - "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderGoogleProps" + "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderGoogleProps", + "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAppleProps" ] }, "stability": "stable", diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-cfdist.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-cfdist.expected.json index e9a67d39c9235..abcb9265e2bda 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-cfdist.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-cfdist.expected.json @@ -71,32 +71,36 @@ ] }, "Create": { - "service": "CognitoIdentityServiceProvider", - "action": "describeUserPoolDomain", - "parameters": { - "Domain": { - "Ref": "UserPoolDomainD0EA232A" - } - }, - "physicalResourceId": { - "id": { - "Ref": "UserPoolDomainD0EA232A" - } - } + "Fn::Join": [ + "", + [ + "{\"service\":\"CognitoIdentityServiceProvider\",\"action\":\"describeUserPoolDomain\",\"parameters\":{\"Domain\":\"", + { + "Ref": "UserPoolDomainD0EA232A" + }, + "\"},\"physicalResourceId\":{\"id\":\"", + { + "Ref": "UserPoolDomainD0EA232A" + }, + "\"}}" + ] + ] }, "Update": { - "service": "CognitoIdentityServiceProvider", - "action": "describeUserPoolDomain", - "parameters": { - "Domain": { - "Ref": "UserPoolDomainD0EA232A" - } - }, - "physicalResourceId": { - "id": { - "Ref": "UserPoolDomainD0EA232A" - } - } + "Fn::Join": [ + "", + [ + "{\"service\":\"CognitoIdentityServiceProvider\",\"action\":\"describeUserPoolDomain\",\"parameters\":{\"Domain\":\"", + { + "Ref": "UserPoolDomainD0EA232A" + }, + "\"},\"physicalResourceId\":{\"id\":\"", + { + "Ref": "UserPoolDomainD0EA232A" + }, + "\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -142,7 +146,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904" }, "S3Key": { "Fn::Join": [ @@ -155,7 +159,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -168,7 +172,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -209,17 +213,17 @@ } }, "Parameters": { - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904": { "Type": "String", - "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 bucket for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF": { "Type": "String", - "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 key for asset version \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343ArtifactHash0C561FF5": { "Type": "String", - "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "Artifact hash for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.apple.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.apple.expected.json new file mode 100644 index 0000000000000..64baf88ef6807 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.apple.expected.json @@ -0,0 +1,118 @@ +{ + "Resources": { + "pool056F3F7E": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AccountRecoverySetting": { + "RecoveryMechanisms": [ + { + "Name": "verified_phone_number", + "Priority": 1 + }, + { + "Name": "verified_email", + "Priority": 2 + } + ] + }, + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsVerificationMessage": "The verification code to your new account is {####}", + "VerificationMessageTemplate": { + "DefaultEmailOption": "CONFIRM_WITH_CODE", + "EmailMessage": "The verification code to your new account is {####}", + "EmailSubject": "Verify your new account", + "SmsMessage": "The verification code to your new account is {####}" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "poolclient2623294C": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "pool056F3F7E" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + { + "Ref": "apple9B5408AC" + }, + "COGNITO" + ] + } + }, + "pooldomain430FA744": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "nija-test-pool", + "UserPoolId": { + "Ref": "pool056F3F7E" + } + } + }, + "apple9B5408AC": { + "Type": "AWS::Cognito::UserPoolIdentityProvider", + "Properties": { + "ProviderName": "SignInWithApple", + "ProviderType": "SignInWithApple", + "UserPoolId": { + "Ref": "pool056F3F7E" + }, + "AttributeMapping": { + "family_name": "lastName", + "given_name": "firstName" + }, + "ProviderDetails": { + "client_id": "com.amzn.cdk", + "team_id": "CDKTEAMCDK", + "key_id": "CDKKEYCDK1", + "private_key": "PRIV_KEY_CDK", + "authorize_scopes": "email name" + } + } + } + }, + "Outputs": { + "SignInLink": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "pooldomain430FA744" + }, + ".auth.", + { + "Ref": "AWS::Region" + }, + ".amazoncognito.com/login?client_id=", + { + "Ref": "poolclient2623294C" + }, + "&response_type=code&redirect_uri=https://example.com" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.apple.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.apple.ts new file mode 100644 index 0000000000000..fb8e15f26e308 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.apple.ts @@ -0,0 +1,41 @@ +import { App, CfnOutput, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderApple } from '../lib'; + +/* + * Stack verification steps + * * Visit the URL provided by stack output 'SignInLink' in a browser, and verify the 'Sign In With Apple' link shows up. + * * If you plug in valid 'Sign In With Apple' credentials, the federated log in should work. + */ +const app = new App(); +const stack = new Stack(app, 'integ-user-pool-idp-apple'); + +const userpool = new UserPool(stack, 'pool', { + removalPolicy: RemovalPolicy.DESTROY, +}); + +new UserPoolIdentityProviderApple(stack, 'apple', { + userPool: userpool, + clientId: 'com.amzn.cdk', + teamId: 'CDKTEAMCDK', + keyId: 'CDKKEYCDK1', + privateKey: 'PRIV_KEY_CDK', + scopes: ['email', 'name'], + attributeMapping: { + familyName: ProviderAttribute.APPLE_LAST_NAME, + givenName: ProviderAttribute.APPLE_FIRST_NAME, + }, +}); + +const client = userpool.addClient('client'); + +const domain = userpool.addDomain('domain', { + cognitoDomain: { + domainPrefix: 'nija-test-pool', + }, +}); + +new CfnOutput(stack, 'SignInLink', { + value: domain.signInUrl(client, { + redirectUri: 'https://example.com', + }), +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts index 3a056cd02dda7..02420721df344 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-client.test.ts @@ -487,13 +487,14 @@ describe('User Pool Client', () => { UserPoolClientIdentityProvider.FACEBOOK, UserPoolClientIdentityProvider.AMAZON, UserPoolClientIdentityProvider.GOOGLE, + UserPoolClientIdentityProvider.APPLE, ], }); // THEN expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { ClientName: 'AllEnabled', - SupportedIdentityProviders: ['COGNITO', 'Facebook', 'LoginWithAmazon', 'Google'], + SupportedIdentityProviders: ['COGNITO', 'Facebook', 'LoginWithAmazon', 'Google', 'SignInWithApple'], }); }); diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/apple.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/apple.ts new file mode 100644 index 0000000000000..5f4180bce5682 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/apple.ts @@ -0,0 +1,113 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderApple } from '../../lib'; + +describe('UserPoolIdentityProvider', () => { + describe('apple', () => { + test('defaults', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderApple(stack, 'userpoolidp', { + userPool: pool, + clientId: 'com.amzn.cdk', + teamId: 'CDKTEAMCDK', + keyId: 'CDKKEYCDK1', + privateKey: 'PRIV_KEY_CDK', + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'SignInWithApple', + ProviderType: 'SignInWithApple', + ProviderDetails: { + client_id: 'com.amzn.cdk', + team_id: 'CDKTEAMCDK', + key_id: 'CDKKEYCDK1', + private_key: 'PRIV_KEY_CDK', + authorize_scopes: 'name', + }, + }); + }); + + test('scopes', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderApple(stack, 'userpoolidp', { + userPool: pool, + clientId: 'com.amzn.cdk', + teamId: 'CDKTEAMCDK', + keyId: 'CDKKEYCDK1', + privateKey: 'PRIV_KEY_CDK', + scopes: ['scope1', 'scope2'], + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'SignInWithApple', + ProviderType: 'SignInWithApple', + ProviderDetails: { + client_id: 'com.amzn.cdk', + team_id: 'CDKTEAMCDK', + key_id: 'CDKKEYCDK1', + private_key: 'PRIV_KEY_CDK', + authorize_scopes: 'scope1 scope2', + }, + }); + }); + + test('registered with user pool', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + const provider = new UserPoolIdentityProviderApple(stack, 'userpoolidp', { + userPool: pool, + clientId: 'com.amzn.cdk', + teamId: 'CDKTEAMCDK', + keyId: 'CDKKEYCDK1', + privateKey: 'PRIV_KEY_CDK', + }); + + // THEN + expect(pool.identityProviders).toContain(provider); + }); + + test('attribute mapping', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderApple(stack, 'userpoolidp', { + userPool: pool, + clientId: 'com.amzn.cdk', + teamId: 'CDKTEAMCDK', + keyId: 'CDKKEYCDK1', + privateKey: 'PRIV_KEY_CDK', + attributeMapping: { + familyName: ProviderAttribute.APPLE_LAST_NAME, + givenName: ProviderAttribute.APPLE_FIRST_NAME, + custom: { + customAttr1: ProviderAttribute.APPLE_EMAIL, + customAttr2: ProviderAttribute.other('sub'), + }, + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + AttributeMapping: { + family_name: 'firstName', + given_name: 'lastName', + customAttr1: 'email', + customAttr2: 'sub', + }, + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-config/README.md b/packages/@aws-cdk/aws-config/README.md index 3d3e24f361549..0a8219a8c3f53 100644 --- a/packages/@aws-cdk/aws-config/README.md +++ b/packages/@aws-cdk/aws-config/README.md @@ -237,7 +237,7 @@ import * as targets from '@aws-cdk/aws-events-targets'; const evalComplianceFn = new lambda.Function(this, 'CustomFunction', { code: lambda.AssetCode.fromInline('exports.handler = (event) => console.log(event);'), handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, }); // A custom rule that runs on configuration changes of EC2 instances diff --git a/packages/@aws-cdk/aws-docdb/README.md b/packages/@aws-cdk/aws-docdb/README.md index 826033de4c134..530942578a090 100644 --- a/packages/@aws-cdk/aws-docdb/README.md +++ b/packages/@aws-cdk/aws-docdb/README.md @@ -30,7 +30,7 @@ your instances will be launched privately or publicly: ```ts const cluster = new DatabaseCluster(this, 'Database', { masterUser: { - username: 'admin' + username: 'myuser' // NOTE: 'admin' is reserved by DocumentDB }, instanceProps: { instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE), 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 8d1e45ac66b83..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 @@ -29,7 +29,7 @@ "devDependencies": { "aws-sdk": "^2.596.0", "aws-sdk-mock": "^5.1.0", - "eslint": "^7.20.0", + "eslint": "^7.21.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-import": "^2.22.1", "eslint-plugin-node": "^11.1.0", @@ -37,6 +37,6 @@ "eslint-plugin-standard": "^4.1.0", "jest": "^26.6.3", "lambda-tester": "^3.6.0", - "nock": "^13.0.7" + "nock": "^13.0.10" } } 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/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 1b12eef42de1f..8f36894fc5df8 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -1670,12 +1670,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 +1693,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 6ca3616c33ac8..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.1" + "ts-jest": "^26.5.3" }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", 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 3b902e5cb8c75..49fff4b5c4f63 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -275,7 +275,7 @@ DatabaseSubnet3 |`ISOLATED`|`10.0.6.32/28`|#3|Only routes within the VPC ### Accessing the Internet Gateway -If you need access to the internet gateway, you can get it's ID like so: +If you need access to the internet gateway, you can get its ID like so: ```ts const igwId = vpc.internetGatewayId; diff --git a/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts b/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts index a190c17d7aa20..646d6b2dcbfa4 100644 --- a/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts +++ b/packages/@aws-cdk/aws-ec2/lib/cfn-init-elements.ts @@ -327,6 +327,9 @@ export abstract class InitFile extends InitElement { * Use a literal string as the file content */ public static fromString(fileName: string, content: string, options: InitFileOptions = {}): InitFile { + if (!content) { + throw new Error(`InitFile ${fileName}: cannot create empty file. Please supply at least one character of content.`); + } return new class extends InitFile { protected _doBind(bindOptions: InitBindOptions) { return { 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/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index 197946716f969..2250bdd37c6c3 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -287,6 +287,7 @@ export class InterfaceVpcEndpointAwsService implements IInterfaceVpcEndpointServ public static readonly KMS = new InterfaceVpcEndpointAwsService('kms'); public static readonly CLOUDWATCH_LOGS = new InterfaceVpcEndpointAwsService('logs'); public static readonly CLOUDWATCH = new InterfaceVpcEndpointAwsService('monitoring'); + public static readonly RDS = new InterfaceVpcEndpointAwsService('rds'); public static readonly SAGEMAKER_API = new InterfaceVpcEndpointAwsService('sagemaker.api'); public static readonly SAGEMAKER_RUNTIME = new InterfaceVpcEndpointAwsService('sagemaker.runtime'); public static readonly SAGEMAKER_RUNTIME_FIPS = new InterfaceVpcEndpointAwsService('sagemaker.runtime-fips'); diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 97f9edc2c3300..801040d4c7385 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -72,6 +72,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -232,6 +233,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.KINESIS_STREAMS", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.KINESIS_FIREHOSE", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.KMS", + "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.RDS", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_API", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_NOTEBOOK", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_RUNTIME", diff --git a/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts b/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts index e794021c46b8f..75896912f3661 100644 --- a/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/cfn-init-element.test.ts @@ -206,6 +206,12 @@ describe('InitFile', () => { }); }); + test('empty content string throws error', () => { + expect(() => { + ec2.InitFile.fromString('/tmp/foo', ''); + }).toThrow('InitFile /tmp/foo: cannot create empty file. Please supply at least one character of content.'); + }); + test('symlink throws an error if mode is set incorrectly', () => { expect(() => { ec2.InitFile.symlink('/tmp/foo', '/tmp/bar', { diff --git a/packages/@aws-cdk/aws-ec2/test/volume.test.ts b/packages/@aws-cdk/aws-ec2/test/volume.test.ts index edd0323bc84ef..c390821c3a37a 100644 --- a/packages/@aws-cdk/aws-ec2/test/volume.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/volume.test.ts @@ -11,6 +11,7 @@ import { } from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior, testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import { AmazonLinuxGeneration, @@ -575,7 +576,7 @@ describe('volume', () => { }); - testFutureBehavior('with future flag aws-kms:defaultKeyPolicies', { '@aws-cdk/aws-kms:defaultKeyPolicies': true }, cdk.App, (app) => { + testFutureBehavior('with future flag aws-kms:defaultKeyPolicies', { [cxapi.KMS_DEFAULT_KEY_POLICIES]: true }, cdk.App, (app) => { // GIVEN const stack = new cdk.Stack(app); const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); 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-assets/test/image-asset.test.ts b/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts index 83f27e16a3857..17fd37be2e234 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/image-asset.test.ts @@ -5,27 +5,19 @@ import * as iam from '@aws-cdk/aws-iam'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { App, DefaultStackSynthesizer, IgnoreMode, Lazy, LegacyStackSynthesizer, Stack, Stage } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import { DockerImageAsset } from '../lib'; /* eslint-disable quote-props */ const DEMO_IMAGE_ASSET_HASH = 'b2c69bfbfe983b634456574587443159b3b7258849856a118ad3d2772238f1a5'; - -let app: App; -let stack: Stack; -beforeEach(() => { - app = new App({ - context: { - '@aws-cdk/aws-ecr-assets:dockerIgnoreSupport': true, - }, - }); - stack = new Stack(app, 'Stack'); -}); +const flags = { [cxapi.DOCKER_IGNORE_SUPPORT]: true }; describe('image asset', () => { - test('test instantiating Asset Image', () => { + testFutureBehavior('test instantiating Asset Image', flags, App, (app) => { // WHEN + const stack = new Stack(app); new DockerImageAsset(stack, 'Image', { directory: path.join(__dirname, 'demo-image'), }); @@ -47,8 +39,9 @@ describe('image asset', () => { }); - test('with build args', () => { + testFutureBehavior('with build args', flags, App, (app) => { // WHEN + const stack = new Stack(app); new DockerImageAsset(stack, 'Image', { directory: path.join(__dirname, 'demo-image'), buildArgs: { @@ -62,8 +55,9 @@ describe('image asset', () => { }); - test('with target', () => { + testFutureBehavior('with target', flags, App, (app) => { // WHEN + const stack = new Stack(app); new DockerImageAsset(stack, 'Image', { directory: path.join(__dirname, 'demo-image'), buildArgs: { @@ -78,8 +72,9 @@ describe('image asset', () => { }); - test('with file', () => { + testFutureBehavior('with file', flags, App, (app) => { // GIVEN + const stack = new Stack(app); const directoryPath = path.join(__dirname, 'demo-image-custom-docker-file'); // WHEN new DockerImageAsset(stack, 'Image', { @@ -93,8 +88,9 @@ describe('image asset', () => { }); - test('asset.repository.grantPull can be used to grant a principal permissions to use the image', () => { + testFutureBehavior('asset.repository.grantPull can be used to grant a principal permissions to use the image', flags, App, (app) => { // GIVEN + const stack = new Stack(app); const user = new iam.User(stack, 'MyUser'); const asset = new DockerImageAsset(stack, 'Image', { directory: path.join(__dirname, 'demo-image'), @@ -155,6 +151,7 @@ describe('image asset', () => { }); test('fails if the directory does not exist', () => { + const stack = new Stack(); // THEN expect(() => { new DockerImageAsset(stack, 'MyAsset', { @@ -165,6 +162,7 @@ describe('image asset', () => { }); test('fails if the directory does not contain a Dockerfile', () => { + const stack = new Stack(); // THEN expect(() => { new DockerImageAsset(stack, 'Asset', { @@ -175,6 +173,7 @@ describe('image asset', () => { }); test('fails if the file does not exist', () => { + const stack = new Stack(); // THEN expect(() => { new DockerImageAsset(stack, 'Asset', { @@ -185,7 +184,8 @@ describe('image asset', () => { }); - test('docker directory is staged if asset staging is enabled', () => { + testFutureBehavior('docker directory is staged if asset staging is enabled', flags, App, (app) => { + const stack = new Stack(app); const image = new DockerImageAsset(stack, 'MyAsset', { directory: path.join(__dirname, 'demo-image'), }); @@ -197,15 +197,16 @@ describe('image asset', () => { }); - test('docker directory is staged without files specified in .dockerignore', () => { - testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(); + testFutureBehavior('docker directory is staged without files specified in .dockerignore', flags, App, (app) => { + testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(app); }); - test('docker directory is staged without files specified in .dockerignore with IgnoreMode.GLOB', () => { - testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(IgnoreMode.GLOB); + testFutureBehavior('docker directory is staged without files specified in .dockerignore with IgnoreMode.GLOB', flags, App, (app) => { + testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(app, IgnoreMode.GLOB); }); - test('docker directory is staged with whitelisted files specified in .dockerignore', () => { + testFutureBehavior('docker directory is staged with whitelisted files specified in .dockerignore', flags, App, (app) => { + const stack = new Stack(app); const image = new DockerImageAsset(stack, 'MyAsset', { directory: path.join(__dirname, 'whitelisted-image'), }); @@ -227,16 +228,17 @@ describe('image asset', () => { }); - test('docker directory is staged without files specified in exclude option', () => { - testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(); + testFutureBehavior('docker directory is staged without files specified in exclude option', flags, App, (app) => { + testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(app); }); - test('docker directory is staged without files specified in exclude option with IgnoreMode.GLOB', () => { - testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(IgnoreMode.GLOB); + testFutureBehavior('docker directory is staged without files specified in exclude option with IgnoreMode.GLOB', flags, App, (app) => { + testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(app, IgnoreMode.GLOB); }); test('fails if using tokens in build args keys or values', () => { // GIVEN + const stack = new Stack(); const token = Lazy.string({ produce: () => 'foo' }); const expected = /Cannot use tokens in keys or values of "buildArgs" since they are needed before deployment/; @@ -256,6 +258,7 @@ describe('image asset', () => { test('fails if using token as repositoryName', () => { // GIVEN + const stack = new Stack(); const token = Lazy.string({ produce: () => 'foo' }); // THEN @@ -267,8 +270,9 @@ describe('image asset', () => { }); - test('docker build options are included in the asset id', () => { + testFutureBehavior('docker build options are included in the asset id', flags, App, (app) => { // GIVEN + const stack = new Stack(app); const directory = path.join(__dirname, 'demo-image-custom-docker-file'); const asset1 = new DockerImageAsset(stack, 'Asset1', { directory }); @@ -290,7 +294,8 @@ describe('image asset', () => { }); }); -function testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(ignoreMode?: IgnoreMode) { +function testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(app: App, ignoreMode?: IgnoreMode) { + const stack = new Stack(app); const image = new DockerImageAsset(stack, 'MyAsset', { ignoreMode, directory: path.join(__dirname, 'dockerignore-image'), @@ -309,7 +314,8 @@ function testDockerDirectoryIsStagedWithoutFilesSpecifiedInDockerignore(ignoreMo } -function testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(ignoreMode?: IgnoreMode) { +function testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(app: App, ignoreMode?: IgnoreMode) { + const stack = new Stack(app); const image = new DockerImageAsset(stack, 'MyAsset', { directory: path.join(__dirname, 'dockerignore-image'), exclude: ['subdirectory'], @@ -328,7 +334,7 @@ function testDockerDirectoryIsStagedWithoutFilesSpecifiedInExcludeOption(ignoreM } -test('nested assemblies share assets: legacy synth edition', () => { +testFutureBehavior('nested assemblies share assets: legacy synth edition', flags, App, (app) => { // GIVEN const stack1 = new Stack(new Stage(app, 'Stage1'), 'Stack', { synthesizer: new LegacyStackSynthesizer() }); const stack2 = new Stack(new Stage(app, 'Stage2'), 'Stack', { synthesizer: new LegacyStackSynthesizer() }); @@ -354,7 +360,7 @@ test('nested assemblies share assets: legacy synth edition', () => { } }); -test('nested assemblies share assets: default synth edition', () => { +testFutureBehavior('nested assemblies share assets: default synth edition', flags, App, (app) => { // GIVEN const stack1 = new Stack(new Stage(app, 'Stage1'), 'Stack', { synthesizer: new DefaultStackSynthesizer() }); const stack2 = new Stack(new Stage(app, 'Stage2'), 'Stack', { synthesizer: new DefaultStackSynthesizer() }); diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts index 21161938c786f..d6c4c2aac75b7 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.assets-docker.ts @@ -1,11 +1,12 @@ import * as path from 'path'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import * as assets from '../lib'; const app = new cdk.App({ context: { - '@aws-cdk/aws-ecr-assets:dockerIgnoreSupport': true, + [cxapi.DOCKER_IGNORE_SUPPORT]: true, }, }); const stack = new cdk.Stack(app, 'integ-assets-docker'); diff --git a/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.ts b/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.ts index 9a6fe48b172e0..394fedc07ddf6 100644 --- a/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.ts +++ b/packages/@aws-cdk/aws-ecr-assets/test/integ.nested-stacks-docker.ts @@ -1,6 +1,7 @@ import * as path from 'path'; import * as iam from '@aws-cdk/aws-iam'; import { App, CfnOutput, NestedStack, NestedStackProps, Stack, StackProps } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import * as ecr_assets from '../lib'; @@ -29,7 +30,7 @@ class TheParentStack extends Stack { const app = new App({ context: { - '@aws-cdk/aws-ecr-assets:dockerIgnoreSupport': true, + [cxapi.DOCKER_IGNORE_SUPPORT]: true, }, }); new TheParentStack(app, 'nested-stacks-docker'); diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 83c05ba1ed308..20f110c206428 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -39,6 +39,15 @@ export interface IRepository extends IResource { */ repositoryUriForTag(tag?: string): string; + /** + * Returns the URI of the repository for a certain tag. Can be used in `docker push/pull`. + * + * ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY[@DIGEST] + * + * @param digest Image digest to use (tools usually default to the image with the "latest" tag if omitted) + */ + repositoryUriForDigest(digest?: string): string; + /** * Add a policy statement to the repository's resource policy */ @@ -136,8 +145,29 @@ export abstract class RepositoryBase extends Resource implements IRepository { */ public repositoryUriForTag(tag?: string): string { const tagSuffix = tag ? `:${tag}` : ''; + return this.repositoryUriWithSuffix(tagSuffix); + } + + /** + * Returns the URL of the repository. Can be used in `docker push/pull`. + * + * ACCOUNT.dkr.ecr.REGION.amazonaws.com/REPOSITORY[@DIGEST] + * + * @param digest Optional image digest + */ + public repositoryUriForDigest(digest?: string): string { + const digestSuffix = digest ? `@${digest}` : ''; + return this.repositoryUriWithSuffix(digestSuffix); + } + + /** + * Returns the repository URI, with an appended suffix, if provided. + * @param suffix An image tag or an image digest. + * @private + */ + private repositoryUriWithSuffix(suffix?: string): string { const parts = this.stack.parseArn(this.repositoryArn); - return `${parts.account}.dkr.ecr.${parts.region}.${this.stack.urlSuffix}/${this.repositoryName}${tagSuffix}`; + return `${parts.account}.dkr.ecr.${parts.region}.${this.stack.urlSuffix}/${this.repositoryName}${suffix}`; } /** @@ -420,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-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index 12f56a2b70102..a1072d47fe700 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -466,3 +466,41 @@ const scheduledFargateTask = new ScheduledFargateTask(stack, 'ScheduledFargateTa platformVersion: ecs.FargatePlatformVersion.VERSION1_4, }); ``` + +### Use the REMOVE_DEFAULT_DESIRED_COUNT feature flag + +The REMOVE_DEFAULT_DESIRED_COUNT feature flag is used to override the default desiredCount that is autogenerated by the CDK. This will set the desiredCount of any service created by any of the following constructs to be undefined. + +* ApplicationLoadBalancedEc2Service +* ApplicationLoadBalancedFargateService +* NetworkLoadBalancedEc2Service +* NetworkLoadBalancedFargateService +* QueueProcessingEc2Service +* QueueProcessingFargateService + +If a desiredCount is not passed in as input to the above constructs, CloudFormation will either create a new service to start up with a desiredCount of 1, or update an existing service to start up with the same desiredCount as prior to the update. + +To enable the feature flag, ensure that the REMOVE_DEFAULT_DESIRED_COUNT flag within an application stack context is set to true, like so: + +```ts +stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); +``` + +The following is an example of an application with the REMOVE_DEFAULT_DESIRED_COUNT feature flag enabled: + +```ts +const app = new App(); + +const stack = new Stack(app, 'aws-ecs-patterns-queue'); +stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); + +const vpc = new ec2.Vpc(stack, 'VPC', { + maxAzs: 2, +}); + +new QueueProcessingFargateService(stack, 'QueueProcessingService', { + vpc, + memoryLimitMiB: 512, + image: new ecs.AssetImage(path.join(__dirname, '..', 'sqs-reader')), +}); +``` diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts index 74411bb217558..dde07a16e114c 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts @@ -78,7 +78,9 @@ export interface ApplicationLoadBalancedServiceBaseProps { * The desired number of instantiations of the task definition to keep running on the service. * The minimum value is 1 * - * @default 1 + * @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1; + * if true, the default is 1 for all new services and uses the existing services desired count + * when updating an existing service. */ readonly desiredCount?: number; @@ -311,12 +313,19 @@ export interface ApplicationLoadBalancedTaskImageOptions { * The base class for ApplicationLoadBalancedEc2Service and ApplicationLoadBalancedFargateService services. */ export abstract class ApplicationLoadBalancedServiceBase extends CoreConstruct { - /** * The desired number of instantiations of the task definition to keep running on the service. + * @deprecated - Use `internalDesiredCount` instead. */ public readonly desiredCount: number; + /** + * The desired number of instantiations of the task definition to keep running on the service. + * The default is 1 for all new services and uses the existing services desired count + * when updating an existing service if one is not provided. + */ + public readonly internalDesiredCount?: number; + /** * The Application Load Balancer for the service. */ @@ -368,7 +377,9 @@ export abstract class ApplicationLoadBalancedServiceBase extends CoreConstruct { if (props.desiredCount !== undefined && props.desiredCount < 1) { throw new Error('You must specify a desiredCount greater than 0'); } + this.desiredCount = props.desiredCount || 1; + this.internalDesiredCount = props.desiredCount; const internetFacing = props.publicLoadBalancer ?? true; diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts index f3eb132e934ae..ff3fc675fcd69 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts @@ -47,7 +47,9 @@ export interface ApplicationMultipleTargetGroupsServiceBaseProps { /** * The desired number of instantiations of the task definition to keep running on the service. * - * @default 1 + * @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1; + * if true, the default is 1 for all new services and uses the existing services desired count + * when updating an existing service. */ readonly desiredCount?: number; @@ -329,12 +331,19 @@ export interface ApplicationListenerProps { * The base class for ApplicationMultipleTargetGroupsEc2Service and ApplicationMultipleTargetGroupsFargateService classes. */ export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreConstruct { - /** * The desired number of instantiations of the task definition to keep running on the service. + * @deprecated - Use `internalDesiredCount` instead. */ public readonly desiredCount: number; + /** + * The desired number of instantiations of the task definition to keep running on the service. + * The default is 1 for all new services and uses the existing services desired count + * when updating an existing service, if one is not provided. + */ + public readonly internalDesiredCount?: number; + /** * The default Application Load Balancer for the service (first added load balancer). */ @@ -365,7 +374,10 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends CoreCon this.validateInput(props); this.cluster = props.cluster || this.getDefaultCluster(this, props.vpc); + this.desiredCount = props.desiredCount || 1; + this.internalDesiredCount = props.desiredCount; + if (props.taskImageOptions) { this.logDriver = this.createLogDriver(props.taskImageOptions.enableLogging, props.taskImageOptions.logDriver); } diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts index 656cc19d19d43..949b052aebbad 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts @@ -67,7 +67,9 @@ export interface NetworkLoadBalancedServiceBaseProps { * The desired number of instantiations of the task definition to keep running on the service. * The minimum value is 1 * - * @default 1 + * @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1; + * if true, the default is 1 for all new services and uses the existing services desired count + * when updating an existing service. */ readonly desiredCount?: number; @@ -263,9 +265,17 @@ export interface NetworkLoadBalancedTaskImageOptions { export abstract class NetworkLoadBalancedServiceBase extends CoreConstruct { /** * The desired number of instantiations of the task definition to keep running on the service. + * @deprecated - Use `internalDesiredCount` instead. */ public readonly desiredCount: number; + /** + * The desired number of instantiations of the task definition to keep running on the service. + * The default is 1 for all new services and uses the existing services desired count + * when updating an existing service, if one is not provided. + */ + public readonly internalDesiredCount?: number; + /** * The Network Load Balancer for the service. */ @@ -306,7 +316,9 @@ export abstract class NetworkLoadBalancedServiceBase extends CoreConstruct { if (props.desiredCount !== undefined && props.desiredCount < 1) { throw new Error('You must specify a desiredCount greater than 0'); } + this.desiredCount = props.desiredCount || 1; + this.internalDesiredCount = props.desiredCount; const internetFacing = props.publicLoadBalancer ?? true; diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts index d34a6b548076d..60fd9904b8078 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-multiple-target-groups-service-base.ts @@ -45,7 +45,9 @@ export interface NetworkMultipleTargetGroupsServiceBaseProps { * The desired number of instantiations of the task definition to keep running on the service. * The minimum value is 1 * - * @default 1 + * @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1; + * if true, the default is 1 for all new services and uses the existing services desired count + * when updating an existing service. */ readonly desiredCount?: number; @@ -264,9 +266,17 @@ export interface NetworkTargetProps { export abstract class NetworkMultipleTargetGroupsServiceBase extends CoreConstruct { /** * The desired number of instantiations of the task definition to keep running on the service. + * @deprecated - Use `internalDesiredCount` instead. */ public readonly desiredCount: number; + /** + * The desired number of instantiations of the task definition to keep running on the service. + * The default is 1 for all new services and uses the existing services desired count + * when updating an existing service, if one is not provided. + */ + public readonly internalDesiredCount?: number; + /** * The Network Load Balancer for the service. */ @@ -297,7 +307,10 @@ export abstract class NetworkMultipleTargetGroupsServiceBase extends CoreConstru this.validateInput(props); this.cluster = props.cluster || this.getDefaultCluster(this, props.vpc); + this.desiredCount = props.desiredCount || 1; + this.internalDesiredCount = props.desiredCount; + if (props.taskImageOptions) { this.logDriver = this.createLogDriver(props.taskImageOptions.enableLogging, props.taskImageOptions.logDriver); } diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts index 3248514931f4d..2f72c6345c469 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts @@ -3,6 +3,7 @@ import { IVpc } from '@aws-cdk/aws-ec2'; import { AwsLogDriver, BaseService, Cluster, ContainerImage, DeploymentController, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; import { IQueue, Queue } from '@aws-cdk/aws-sqs'; import { CfnOutput, Duration, Stack } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. @@ -53,7 +54,10 @@ export interface QueueProcessingServiceBaseProps { /** * The desired number of instantiations of the task definition to keep running on the service. * - * @default 1 + * @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is 1; + * if true, the minScalingCapacity is 1 for all new services and uses the existing services desired count + * when updating an existing service. + * @deprecated - Use `minScalingCapacity` or a literal object instead. */ readonly desiredTaskCount?: number; @@ -109,10 +113,17 @@ export interface QueueProcessingServiceBaseProps { /** * Maximum capacity to scale to. * - * @default (desiredTaskCount * 2) + * @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is (desiredTaskCount * 2); if true, the default is 2. */ readonly maxScalingCapacity?: number + /** + * Minimum capacity to scale to. + * + * @default - If the feature flag, ECS_REMOVE_DEFAULT_DESIRED_COUNT is false, the default is the desiredTaskCount; if true, the default is 1. + */ + readonly minScalingCapacity?: number + /** * The intervals for scaling based on the SQS queue's ApproximateNumberOfMessagesVisible metric. * @@ -214,6 +225,7 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct { /** * The minimum number of tasks to run. + * @deprecated - Use `minCapacity` instead. */ public readonly desiredCount: number; @@ -222,6 +234,11 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct { */ public readonly maxCapacity: number; + /** + * The minimum number of instances for autoscaling to scale down to. + */ + public readonly minCapacity: number; + /** * The scaling interval for autoscaling based off an SQS Queue size. */ @@ -272,9 +289,21 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct { this.environment = { ...(props.environment || {}), QUEUE_NAME: this.sqsQueue.queueName }; this.secrets = props.secrets; - // Determine the desired task count (minimum) and maximum scaling capacity this.desiredCount = props.desiredTaskCount ?? 1; - this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount); + + // Determine the desired task count (minimum) and maximum scaling capacity + if (!this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT)) { + this.minCapacity = props.minScalingCapacity || this.desiredCount; + this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount); + } else { + if (props.desiredTaskCount != null) { + this.minCapacity = props.minScalingCapacity || this.desiredCount; + this.maxCapacity = props.maxScalingCapacity || (2 * this.desiredCount); + } else { + this.minCapacity = props.minScalingCapacity || 1; + this.maxCapacity = props.maxScalingCapacity || 2; + } + } if (!this.desiredCount && !this.maxCapacity) { throw new Error('maxScalingCapacity must be set and greater than 0 if desiredCount is 0'); @@ -290,7 +319,7 @@ export abstract class QueueProcessingServiceBase extends CoreConstruct { * @param service the ECS/Fargate service for which to apply the autoscaling rules to */ protected configureAutoscalingForService(service: BaseService) { - const scalingTarget = service.autoScaleTaskCount({ maxCapacity: this.maxCapacity, minCapacity: this.desiredCount }); + const scalingTarget = service.autoScaleTaskCount({ maxCapacity: this.maxCapacity, minCapacity: this.minCapacity }); scalingTarget.scaleOnCpuUtilization('CpuScaling', { targetUtilizationPercent: 50, }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts index b9bdcf2d100ad..2915fce6a48ff 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts @@ -1,4 +1,5 @@ import { Ec2Service, Ec2TaskDefinition } from '@aws-cdk/aws-ecs'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { ApplicationLoadBalancedServiceBase, ApplicationLoadBalancedServiceBaseProps } from '../base/application-load-balanced-service-base'; @@ -117,9 +118,11 @@ export class ApplicationLoadBalancedEc2Service extends ApplicationLoadBalancedSe throw new Error('You must specify one of: taskDefinition or image'); } + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + this.service = new Ec2Service(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: false, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts index 6ed6b6b71802f..90f4afdd5fa8f 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-multiple-target-groups-ecs-service.ts @@ -1,5 +1,6 @@ import { Ec2Service, Ec2TaskDefinition } from '@aws-cdk/aws-ecs'; import { ApplicationTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { ApplicationMultipleTargetGroupsServiceBase, @@ -136,9 +137,11 @@ export class ApplicationMultipleTargetGroupsEc2Service extends ApplicationMultip } private createEc2Service(props: ApplicationMultipleTargetGroupsEc2ServiceProps): Ec2Service { + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + return new Ec2Service(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: false, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts index fae46b68e7380..881a346c74f8a 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts @@ -1,4 +1,5 @@ import { Ec2Service, Ec2TaskDefinition } from '@aws-cdk/aws-ecs'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { NetworkLoadBalancedServiceBase, NetworkLoadBalancedServiceBaseProps } from '../base/network-load-balanced-service-base'; @@ -115,9 +116,11 @@ export class NetworkLoadBalancedEc2Service extends NetworkLoadBalancedServiceBas throw new Error('You must specify one of: taskDefinition or image'); } + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + this.service = new Ec2Service(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: false, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts index eb8392d3b2148..f0d3b0a1571ce 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-multiple-target-groups-ecs-service.ts @@ -1,5 +1,6 @@ import { Ec2Service, Ec2TaskDefinition } from '@aws-cdk/aws-ecs'; import { NetworkTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { NetworkMultipleTargetGroupsServiceBase, @@ -136,9 +137,11 @@ export class NetworkMultipleTargetGroupsEc2Service extends NetworkMultipleTarget } private createEc2Service(props: NetworkMultipleTargetGroupsEc2ServiceProps): Ec2Service { + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + return new Ec2Service(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: false, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts index f9d9b98810aa0..6858813dfa9cc 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts @@ -1,4 +1,5 @@ import { Ec2Service, Ec2TaskDefinition } from '@aws-cdk/aws-ecs'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { QueueProcessingServiceBase, QueueProcessingServiceBaseProps } from '../base/queue-processing-service-base'; @@ -98,11 +99,14 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { logging: this.logDriver, }); + // The desiredCount should be removed from the fargate service when the feature flag is removed. + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? undefined : this.desiredCount; + // Create an ECS service with the previously defined Task Definition and configure // autoscaling based on cpu utilization and number of messages visible in the SQS queue. this.service = new Ec2Service(this, 'QueueProcessingService', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, serviceName: props.serviceName, minHealthyPercent: props.minHealthyPercent, @@ -111,6 +115,7 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { enableECSManagedTags: props.enableECSManagedTags, deploymentController: props.deploymentController, }); + this.configureAutoscalingForService(this.service); this.grantPermissionsToService(this.service); } diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts index 2ae468bcae558..fbb68aef84b2f 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts @@ -1,5 +1,6 @@ import { ISecurityGroup, SubnetSelection } from '@aws-cdk/aws-ec2'; import { FargatePlatformVersion, FargateService, FargateTaskDefinition } from '@aws-cdk/aws-ecs'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { ApplicationLoadBalancedServiceBase, ApplicationLoadBalancedServiceBaseProps } from '../base/application-load-balanced-service-base'; @@ -153,9 +154,11 @@ export class ApplicationLoadBalancedFargateService extends ApplicationLoadBalanc throw new Error('You must specify one of: taskDefinition or image'); } + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + this.service = new FargateService(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: this.assignPublicIp, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts index 495049dfccfa8..6759e8e001376 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-multiple-target-groups-fargate-service.ts @@ -1,5 +1,6 @@ import { FargatePlatformVersion, FargateService, FargateTaskDefinition } from '@aws-cdk/aws-ecs'; import { ApplicationTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { ApplicationMultipleTargetGroupsServiceBase, @@ -168,9 +169,11 @@ export class ApplicationMultipleTargetGroupsFargateService extends ApplicationMu } private createFargateService(props: ApplicationMultipleTargetGroupsFargateServiceProps): FargateService { + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + return new FargateService(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: this.assignPublicIp, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts index 4aad4b31e7efe..404d5429acfed 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts @@ -1,5 +1,6 @@ import { SubnetSelection } from '@aws-cdk/aws-ec2'; import { FargatePlatformVersion, FargateService, FargateTaskDefinition } from '@aws-cdk/aws-ecs'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { NetworkLoadBalancedServiceBase, NetworkLoadBalancedServiceBaseProps } from '../base/network-load-balanced-service-base'; @@ -140,9 +141,11 @@ export class NetworkLoadBalancedFargateService extends NetworkLoadBalancedServic throw new Error('You must specify one of: taskDefinition or image'); } + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + this.service = new FargateService(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: this.assignPublicIp, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts index dab033b1938ce..4a4974af7cce3 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-multiple-target-groups-fargate-service.ts @@ -1,5 +1,6 @@ import { FargatePlatformVersion, FargateService, FargateTaskDefinition } from '@aws-cdk/aws-ecs'; import { NetworkTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { NetworkMultipleTargetGroupsServiceBase, @@ -168,9 +169,11 @@ export class NetworkMultipleTargetGroupsFargateService extends NetworkMultipleTa } private createFargateService(props: NetworkMultipleTargetGroupsFargateServiceProps): FargateService { + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? this.internalDesiredCount : this.desiredCount; + return new FargateService(this, 'Service', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, assignPublicIp: this.assignPublicIp, serviceName: props.serviceName, diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts index e3712b00ae4b1..6444d05f81da6 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts @@ -1,5 +1,6 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import { FargatePlatformVersion, FargateService, FargateTaskDefinition } from '@aws-cdk/aws-ecs'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { QueueProcessingServiceBase, QueueProcessingServiceBaseProps } from '../base/queue-processing-service-base'; @@ -128,11 +129,14 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { logging: this.logDriver, }); + // The desiredCount should be removed from the fargate service when the feature flag is removed. + const desiredCount = this.node.tryGetContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT) ? undefined : this.desiredCount; + // Create a Fargate service with the previously defined Task Definition and configure // autoscaling based on cpu utilization and number of messages visible in the SQS queue. this.service = new FargateService(this, 'QueueProcessingFargateService', { cluster: this.cluster, - desiredCount: this.desiredCount, + desiredCount: desiredCount, taskDefinition: this.taskDefinition, serviceName: props.serviceName, minHealthyPercent: props.minHealthyPercent, @@ -145,6 +149,7 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { vpcSubnets: props.taskSubnets, assignPublicIp: props.assignPublicIp, }); + this.configureAutoscalingForService(this.service); this.grantPermissionsToService(this.service); } diff --git a/packages/@aws-cdk/aws-ecs-patterns/package.json b/packages/@aws-cdk/aws-ecs-patterns/package.json index bc2a816cd6b11..dcb4d8b436bda 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/package.json +++ b/packages/@aws-cdk/aws-ecs-patterns/package.json @@ -86,6 +86,7 @@ "@aws-cdk/aws-servicediscovery": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^3.2.0" }, "homepage": "https://github.com/aws/aws-cdk", @@ -103,6 +104,7 @@ "@aws-cdk/aws-servicediscovery": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^3.2.0" }, "engines": { diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.multiple-application-load-balanced-ecs-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.multiple-application-load-balanced-ecs-service.expected.json index 147a8bc8a7bfb..ec9686e054019 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.multiple-application-load-balanced-ecs-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.multiple-application-load-balanced-ecs-service.expected.json @@ -719,14 +719,12 @@ "Code": { "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n while has_tasks(cluster, instance_arn):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\n\ndef has_tasks(cluster, instance_arn):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n tasks = instance['runningTasksCount'] + instance['pendingTasksCount']\n print('Instance %s has %s tasks' % (instance_arn, tasks))\n\n return tasks > 0\n\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" }, - "Handler": "index.lambda_handler", "Role": { "Fn::GetAtt": [ "ClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole2AC250B1", "Arn" ] }, - "Runtime": "python3.6", "Environment": { "Variables": { "CLUSTER": { @@ -734,6 +732,8 @@ } } }, + "Handler": "index.lambda_handler", + "Runtime": "python3.6", "Tags": [ { "Key": "Name", @@ -1128,7 +1128,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "EC2", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json index 5d4345573b0c5..53c05f9aeba8e 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/integ.scheduled-ecs-task.lit.expected.json @@ -536,14 +536,12 @@ "Code": { "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n while has_tasks(cluster, instance_arn):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\n\ndef has_tasks(cluster, instance_arn):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n tasks = instance['runningTasksCount'] + instance['pendingTasksCount']\n print('Instance %s has %s tasks' % (instance_arn, tasks))\n\n return tasks > 0\n\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" }, - "Handler": "index.lambda_handler", "Role": { "Fn::GetAtt": [ "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA", "Arn" ] }, - "Runtime": "python3.6", "Environment": { "Variables": { "CLUSTER": { @@ -551,6 +549,8 @@ } } }, + "Handler": "index.lambda_handler", + "Runtime": "python3.6", "Tags": [ { "Key": "Name", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts index 959a6348fde11..6f5a4aa0b3337 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts @@ -1,4 +1,4 @@ -import { arrayWith, expect, haveResource, haveResourceLike, objectLike } from '@aws-cdk/assert'; +import { ABSENT, arrayWith, expect, haveResource, haveResourceLike, objectLike } from '@aws-cdk/assert'; import { Certificate } from '@aws-cdk/aws-certificatemanager'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; @@ -6,6 +6,7 @@ import { ApplicationLoadBalancer, ApplicationProtocol, NetworkLoadBalancer } fro import { PublicHostedZone } from '@aws-cdk/aws-route53'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Test } from 'nodeunit'; import * as ecsPatterns from '../../lib'; @@ -60,6 +61,102 @@ export = { test.done(); }, + 'ApplicationLoadBalancedEc2Service desiredCount can be undefined when feature flag is set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedEc2Service(stack, 'Service', { + cluster, + memoryLimitMiB: 1024, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('test'), + }, + }); + + expect(stack).to(haveResource('AWS::ECS::Service', { + DesiredCount: ABSENT, + })); + + test.done(); + }, + + 'ApplicationLoadBalancedFargateService desiredCount can be undefined when feature flag is set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'Service', { + cluster, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('test'), + }, + }); + + expect(stack).to(haveResource('AWS::ECS::Service', { + DesiredCount: ABSENT, + })); + + test.done(); + }, + + 'NetworkLoadBalancedEc2Service desiredCount can be undefined when feature flag is set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // WHEN + new ecsPatterns.NetworkLoadBalancedEc2Service(stack, 'Service', { + cluster, + memoryLimitMiB: 1024, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('test'), + }, + }); + + expect(stack).to(haveResource('AWS::ECS::Service', { + DesiredCount: ABSENT, + })); + + test.done(); + }, + + 'NetworkLoadBalancedFargateService desiredCount can be undefined when feature flag is set'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecsPatterns.NetworkLoadBalancedFargateService(stack, 'Service', { + cluster, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('test'), + }, + }); + + expect(stack).to(haveResource('AWS::ECS::Service', { + DesiredCount: ABSENT, + })); + + test.done(); + }, + 'set vpc instead of cluster'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts index d449cc27db2c4..2c410f6581b1e 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts @@ -1,8 +1,9 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import { ABSENT, expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Test } from 'nodeunit'; import * as ecsPatterns from '../../lib'; @@ -80,6 +81,31 @@ export = { test.done(); }, + 'test ECS queue worker service construct - with remove default desiredCount feature flag'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // WHEN + new ecsPatterns.QueueProcessingEc2Service(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + }); + + // THEN - QueueWorker is of EC2 launch type, and desiredCount is not defined on the Ec2Service. + expect(stack).to(haveResource('AWS::ECS::Service', { + DesiredCount: ABSENT, + LaunchType: 'EC2', + })); + + test.done(); + }, + 'test ECS queue worker service construct - with optional props for queues'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.asset-image.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.asset-image.expected.json index 364f0a27d8b15..4fdde5753fdf6 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.asset-image.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.asset-image.expected.json @@ -637,7 +637,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", @@ -752,4 +751,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.executionrole.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.executionrole.expected.json index 2d70c2c40cae2..93c7a7307a6ee 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.executionrole.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.executionrole.expected.json @@ -589,7 +589,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-autocreate.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-autocreate.expected.json index 84a73622ee2d8..5813cd78e41f3 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-autocreate.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-autocreate.expected.json @@ -228,7 +228,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", @@ -894,7 +893,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-vpconly.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-vpconly.expected.json index c549746d8769e..5556df70a59b7 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-vpconly.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3-vpconly.expected.json @@ -583,7 +583,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", @@ -1249,7 +1248,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", @@ -1560,7 +1558,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3.expected.json index c79a622e159a9..40d86be5d331b 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.l3.expected.json @@ -586,7 +586,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json index 532377e2accdd..1d670f79f58a6 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json @@ -664,7 +664,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": true, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.multiple-network-load-balanced-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.multiple-network-load-balanced-fargate-service.expected.json index 815cfe99b94f6..2cdd6991792e8 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.multiple-network-load-balanced-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.multiple-network-load-balanced-fargate-service.expected.json @@ -597,7 +597,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-isolated.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-isolated.expected.json index b1d88ed107154..6124fba473584 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-isolated.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-isolated.expected.json @@ -903,7 +903,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "LaunchType": "FARGATE", "NetworkConfiguration": { diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json index fd8f791e3a868..de873260aa209 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service-public.expected.json @@ -753,7 +753,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "LaunchType": "FARGATE", "NetworkConfiguration": { diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json index 889eeefcd985e..6fbcb79ecfa00 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.queue-processing-fargate-service.expected.json @@ -594,7 +594,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "LaunchType": "FARGATE", "NetworkConfiguration": { diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.special-listener.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.special-listener.expected.json index 76e5e6a13aaf7..5075f6511573b 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.special-listener.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.special-listener.expected.json @@ -539,7 +539,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", @@ -826,7 +825,6 @@ "MaximumPercent": 200, "MinimumHealthyPercent": 50 }, - "DesiredCount": 1, "EnableECSManagedTags": false, "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts index 9d2d5dd0747a6..27671ae22e70d 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts @@ -1,8 +1,9 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import { ABSENT, expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Test } from 'nodeunit'; import * as ecsPatterns from '../../lib'; @@ -102,6 +103,30 @@ export = { test.done(); }, + 'test fargate queue worker service construct - with remove default desiredCount feature flag'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ECS_REMOVE_DEFAULT_DESIRED_COUNT, true); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecsPatterns.QueueProcessingFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + }); + + // THEN - QueueWorker is of FARGATE launch type, and desiredCount is not defined on the FargateService. + expect(stack).to(haveResource('AWS::ECS::Service', { + DesiredCount: ABSENT, + LaunchType: 'FARGATE', + })); + + test.done(); + }, + 'test fargate queue worker service construct - with optional props for queues'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -199,6 +224,97 @@ export = { test.done(); }, + 'test Fargate queue worker service construct - without desiredCount specified'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const queue = new sqs.Queue(stack, 'fargate-test-queue', { + queueName: 'fargate-test-sqs-queue', + }); + + // WHEN + new ecsPatterns.QueueProcessingFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + command: ['-c', '4', 'amazon.com'], + enableLogging: false, + environment: { + TEST_ENVIRONMENT_VARIABLE1: 'test environment variable 1 value', + TEST_ENVIRONMENT_VARIABLE2: 'test environment variable 2 value', + }, + queue, + maxScalingCapacity: 5, + minScalingCapacity: 2, + minHealthyPercent: 60, + maxHealthyPercent: 150, + serviceName: 'fargate-test-service', + family: 'fargate-task-family', + platformVersion: ecs.FargatePlatformVersion.VERSION1_4, + deploymentController: { + type: ecs.DeploymentControllerType.CODE_DEPLOY, + }, + }); + + // THEN - QueueWorker is of FARGATE launch type, an SQS queue is created and all optional properties are set. + expect(stack).to(haveResource('AWS::ECS::Service', { + DeploymentConfiguration: { + MinimumHealthyPercent: 60, + MaximumPercent: 150, + }, + LaunchType: 'FARGATE', + ServiceName: 'fargate-test-service', + PlatformVersion: ecs.FargatePlatformVersion.VERSION1_4, + DeploymentController: { + Type: 'CODE_DEPLOY', + }, + })); + + expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + MaxCapacity: 5, + MinCapacity: 2, + })); + + expect(stack).to(haveResource('AWS::SQS::Queue', { QueueName: 'fargate-test-sqs-queue' })); + + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Command: [ + '-c', + '4', + 'amazon.com', + ], + Environment: [ + { + Name: 'TEST_ENVIRONMENT_VARIABLE1', + Value: 'test environment variable 1 value', + }, + { + Name: 'TEST_ENVIRONMENT_VARIABLE2', + Value: 'test environment variable 2 value', + }, + { + Name: 'QUEUE_NAME', + Value: { + 'Fn::GetAtt': [ + 'fargatetestqueue28B43841', + 'QueueName', + ], + }, + }, + ], + Image: 'test', + }, + ], + Family: 'fargate-task-family', + })); + + test.done(); + }, + 'test Fargate queue worker service construct - with optional props'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 438f63e14e009..a038cf65fbaf9 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -67,7 +67,7 @@ one to run tasks on AWS Fargate. Here are the main differences: - **Amazon EC2**: instances are under your control. Complete control of task to host - allocation. Required to specify at least a memory reseration or limit for + allocation. Required to specify at least a memory reservation or limit for every container. Can use Host, Bridge and AwsVpc networking modes. Can attach Classic Load Balancer. Can share volumes between container and host. - **AWS Fargate**: tasks run on AWS-managed instances, AWS manages task to host @@ -175,7 +175,7 @@ cluster.addCapacity('AsgSpot', { When the `ecs.AddCapacityOptions` that you provide has a non-zero `taskDrainTime` (the default) then an SNS topic and Lambda are created to ensure that the cluster's instances have been properly drained of tasks before terminating. The SNS Topic is sent the instance-terminating lifecycle event from the AutoScalingGroup, and the Lambda acts on that event. If you wish to engage [server-side encryption](https://docs.aws.amazon.com/sns/latest/dg/sns-data-encryption.html) for this SNS Topic -then you may do so by providing a KMS key for the `topicEncryptionKey` propery of `ecs.AddCapacityOptions`. +then you may do so by providing a KMS key for the `topicEncryptionKey` property of `ecs.AddCapacityOptions`. ```ts // Given @@ -190,7 +190,7 @@ cluster.addCapacity('ASGEncryptedSNS', { ## Task definitions -A task Definition describes what a single copy of a **task** should look like. +A task definition describes what a single copy of a **task** should look like. A task definition has one or more containers; typically, it has one main container (the *default container* is the first one that's added to the task definition, and it is marked *essential*) and optionally @@ -237,23 +237,35 @@ const container = ec2TaskDefinition.addContainer("WebContainer", { You can specify container properties when you add them to the task definition, or with various methods, e.g.: +To add a port mapping when adding a container to the task definition, specify the `portMappings` option: + +```ts +taskDefinition.addContainer("WebContainer", { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + memoryLimitMiB: 1024, + portMappings: [{ containerPort: 3000 }] +}); +``` + +To add port mappings directly to a container definition, call `addPortMappings()`: + ```ts container.addPortMappings({ containerPort: 3000 -}) +}); ``` To add data volumes to a task definition, call `addVolume()`: ```ts -const volume = ecs.Volume("Volume", { +const volume = { // Use an Elastic FileSystem name: "mydatavolume", efsVolumeConfiguration: ecs.EfsVolumeConfiguration({ fileSystemId: "EFS" // ... other options here ... }) -}); +}; const container = fargateTaskDefinition.addVolume("mydatavolume"); ``` @@ -671,6 +683,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/_imported-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/_imported-task-definition.ts new file mode 100644 index 0000000000000..3c9c583b96de0 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs/lib/base/_imported-task-definition.ts @@ -0,0 +1,108 @@ +import { IRole } from '@aws-cdk/aws-iam'; +import { Construct } from 'constructs'; +import { IEc2TaskDefinition } from '../ec2/ec2-task-definition'; +import { IFargateTaskDefinition } from '../fargate/fargate-task-definition'; +import { Compatibility, NetworkMode, isEc2Compatible, isFargateCompatible } from './task-definition'; +import { Resource } from '@aws-cdk/core'; + +/** + * The properties of ImportedTaskDefinition + */ +export interface ImportedTaskDefinitionProps { + /** + * The arn of the task definition + */ + readonly taskDefinitionArn: string; + + /** + * What launch types this task definition should be compatible with. + * + * @default Compatibility.EC2_AND_FARGATE + */ + readonly compatibility?: Compatibility; + + /** + * The networking mode to use for the containers in the task. + * + * @default Network mode cannot be provided to the imported task. + */ + readonly networkMode?: NetworkMode; + + /** + * The name of the IAM role that grants containers in the task permission to call AWS APIs on your behalf. + * + * @default Permissions cannot be granted to the imported task. + */ + readonly taskRole?: IRole; +} + +/** + * Task definition reference of an imported task + */ +export class ImportedTaskDefinition extends Resource implements IEc2TaskDefinition, IFargateTaskDefinition { + /** + * What launch types this task definition should be compatible with. + */ + readonly compatibility: Compatibility; + + /** + * ARN of this task definition + */ + readonly taskDefinitionArn: string; + + /** + * Execution role for this task definition + */ + readonly executionRole?: IRole = undefined; + + /** + * The networking mode to use for the containers in the task. + */ + readonly _networkMode?: NetworkMode; + + /** + * The name of the IAM role that grants containers in the task permission to call AWS APIs on your behalf. + */ + readonly _taskRole?: IRole; + + constructor(scope: Construct, id: string, props: ImportedTaskDefinitionProps) { + super(scope, id); + + this.compatibility = props.compatibility ?? Compatibility.EC2_AND_FARGATE; + this.taskDefinitionArn = props.taskDefinitionArn; + this._taskRole = props.taskRole; + this._networkMode = props.networkMode; + } + + public get networkMode(): NetworkMode { + if (this._networkMode == undefined) { + throw new Error('This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + + 'Add the \'networkMode\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + } else { + return this._networkMode; + } + } + + public get taskRole(): IRole { + if (this._taskRole == undefined) { + throw new Error('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + } else { + return this._taskRole; + } + } + + /** + * Return true if the task definition can be run on an EC2 cluster + */ + public get isEc2Compatible(): boolean { + return isEc2Compatible(this.compatibility); + } + + /** + * Return true if the task definition can be run on a Fargate cluster + */ + public get isFargateCompatible(): boolean { + return isFargateCompatible(this.compatibility); + } +} 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/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index f7b0f05c92800..338fd59b39ea7 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -8,6 +8,7 @@ import { FirelensLogRouter, FirelensLogRouterDefinitionOptions, FirelensLogRoute import { AwsLogDriver } from '../log-drivers/aws-log-driver'; import { PlacementConstraint } from '../placement'; import { ProxyConfiguration } from '../proxy-configuration/proxy-configuration'; +import { ImportedTaskDefinition } from './_imported-task-definition'; /** * The interface for all task definitions. @@ -38,6 +39,16 @@ export interface ITaskDefinition extends IResource { * Return true if the task definition can be run on a Fargate cluster */ readonly isFargateCompatible: boolean; + + /** + * The networking mode to use for the containers in the task. + */ + readonly networkMode: NetworkMode; + + /** + * The name of the IAM role that grants containers in the task permission to call AWS APIs on your behalf. + */ + readonly taskRole: iam.IRole; } /** @@ -175,10 +186,48 @@ export interface TaskDefinitionProps extends CommonTaskDefinitionProps { readonly pidMode?: PidMode; } +/** + * The common task definition attributes used across all types of task definitions. + */ +export interface CommonTaskDefinitionAttributes { + /** + * The arn of the task definition + */ + readonly taskDefinitionArn: string; + + /** + * The networking mode to use for the containers in the task. + * + * @default Network mode cannot be provided to the imported task. + */ + readonly networkMode?: NetworkMode; + + /** + * The name of the IAM role that grants containers in the task permission to call AWS APIs on your behalf. + * + * @default Permissions cannot be granted to the imported task. + */ + readonly taskRole?: iam.IRole; +} + +/** + * A reference to an existing task definition + */ +export interface TaskDefinitionAttributes extends CommonTaskDefinitionAttributes { + /** + * What launch types this task definition should be compatible with. + * + * @default Compatibility.EC2_AND_FARGATE + */ + readonly compatibility?: Compatibility; +} + abstract class TaskDefinitionBase extends Resource implements ITaskDefinition { public abstract readonly compatibility: Compatibility; + public abstract readonly networkMode: NetworkMode; public abstract readonly taskDefinitionArn: string; + public abstract readonly taskRole: iam.IRole; public abstract readonly executionRole?: iam.IRole; /** @@ -207,13 +256,19 @@ export class TaskDefinition extends TaskDefinitionBase { * The task will have a compatibility of EC2+Fargate. */ public static fromTaskDefinitionArn(scope: Construct, id: string, taskDefinitionArn: string): ITaskDefinition { - class Import extends TaskDefinitionBase { - public readonly taskDefinitionArn = taskDefinitionArn; - public readonly compatibility = Compatibility.EC2_AND_FARGATE; - public readonly executionRole?: iam.IRole = undefined; - } + return new ImportedTaskDefinition(scope, id, { taskDefinitionArn: taskDefinitionArn }); + } - return new Import(scope, id); + /** + * Create a task definition from a task definition reference + */ + public static fromTaskDefinitionAttributes(scope: Construct, id: string, attrs: TaskDefinitionAttributes): ITaskDefinition { + return new ImportedTaskDefinition(scope, id, { + taskDefinitionArn: attrs.taskDefinitionArn, + compatibility: attrs.compatibility, + networkMode: attrs.networkMode, + taskRole: attrs.taskRole, + }); } /** @@ -248,7 +303,7 @@ export class TaskDefinition extends TaskDefinitionBase { public defaultContainer?: ContainerDefinition; /** - * The task launch type compatiblity requirement. + * The task launch type compatibility requirement. */ public readonly compatibility: Compatibility; @@ -890,13 +945,13 @@ export interface ITaskDefinitionExtension { /** * Return true if the given task definition can be run on an EC2 cluster */ -function isEc2Compatible(compatibility: Compatibility): boolean { +export function isEc2Compatible(compatibility: Compatibility): boolean { return [Compatibility.EC2, Compatibility.EC2_AND_FARGATE].includes(compatibility); } /** * Return true if the given task definition can be run on a Fargate cluster */ -function isFargateCompatible(compatibility: Compatibility): boolean { +export function isFargateCompatible(compatibility: Compatibility): boolean { return [Compatibility.FARGATE, Compatibility.EC2_AND_FARGATE].includes(compatibility); } diff --git a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts index 983489743482f..9911a49a039cf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/container-definition.ts @@ -288,6 +288,12 @@ export interface ContainerDefinitionOptions { * @default - No GPUs assigned. */ readonly gpuCount?: number; + + /** + * The port mappings to add to the container definition. + * @default - No ports are mapped. + */ + readonly portMappings?: PortMapping[]; } /** @@ -433,6 +439,10 @@ export class ContainerDefinition extends CoreConstruct { } props.taskDefinition._linkContainer(this); + + if (props.portMappings) { + this.addPortMappings(...props.portMappings); + } } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index 18c6df350fb4e..4cf4de8a83292 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -229,9 +229,9 @@ export class Ec2Service extends BaseService implements IEc2Service { this.addPlacementConstraints(...props.placementConstraints || []); this.addPlacementStrategies(...props.placementStrategies || []); - if (!this.taskDefinition.defaultContainer) { - throw new Error('A TaskDefinition must have at least one essential container'); - } + this.node.addValidation({ + validate: () => !this.taskDefinition.defaultContainer ? ['A TaskDefinition must have at least one essential container'] : [], + }); } /** @@ -249,7 +249,7 @@ export class Ec2Service extends BaseService implements IEc2Service { } /** - * Adds one or more placement contstraints to use for tasks in the service. For more information, see + * Adds one or more placement constraints to use for tasks in the service. For more information, see * [Amazon ECS Task Placement Constraints](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-placement-constraints.html). */ public addPlacementConstraints(...constraints: PlacementConstraint[]) { diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts index 67d096830b9c7..ff571c884b73e 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-task-definition.ts @@ -1,7 +1,16 @@ -import { Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { CommonTaskDefinitionProps, Compatibility, IpcMode, ITaskDefinition, NetworkMode, PidMode, TaskDefinition } from '../base/task-definition'; +import { + CommonTaskDefinitionAttributes, + CommonTaskDefinitionProps, + Compatibility, + IpcMode, + ITaskDefinition, + NetworkMode, + PidMode, + TaskDefinition, +} from '../base/task-definition'; import { PlacementConstraint } from '../placement'; +import { ImportedTaskDefinition } from '../base/_imported-task-definition'; /** * The properties for a task definition run on an EC2 cluster. @@ -51,6 +60,13 @@ export interface IEc2TaskDefinition extends ITaskDefinition { } +/** + * Attributes used to import an existing EC2 task definition + */ +export interface Ec2TaskDefinitionAttributes extends CommonTaskDefinitionAttributes { + +} + /** * The details of a task definition run on an EC2 cluster. * @@ -62,13 +78,25 @@ export class Ec2TaskDefinition extends TaskDefinition implements IEc2TaskDefinit * Imports a task definition from the specified task definition ARN. */ public static fromEc2TaskDefinitionArn(scope: Construct, id: string, ec2TaskDefinitionArn: string): IEc2TaskDefinition { - class Import extends Resource implements IEc2TaskDefinition { - public readonly taskDefinitionArn = ec2TaskDefinitionArn; - public readonly compatibility = Compatibility.EC2; - public readonly isEc2Compatible = true; - public readonly isFargateCompatible = false; - } - return new Import(scope, id); + return new ImportedTaskDefinition(scope, id, { + taskDefinitionArn: ec2TaskDefinitionArn, + }); + } + + /** + * Imports an existing Ec2 task definition from its attributes + */ + public static fromEc2TaskDefinitionAttributes( + scope: Construct, + id: string, + attrs: Ec2TaskDefinitionAttributes, + ): IEc2TaskDefinition { + return new ImportedTaskDefinition(scope, id, { + taskDefinitionArn: attrs.taskDefinitionArn, + compatibility: Compatibility.EC2, + networkMode: attrs.networkMode, + taskRole: attrs.taskRole, + }); } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index 1db94fc5286e0..793fb633e83d0 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -172,9 +172,9 @@ export class FargateService extends BaseService implements IFargateService { this.configureAwsVpcNetworkingWithSecurityGroups(props.cluster.vpc, props.assignPublicIp, props.vpcSubnets, securityGroups); - if (!props.taskDefinition.defaultContainer) { - throw new Error('A TaskDefinition must have at least one essential container'); - } + this.node.addValidation({ + validate: () => !this.taskDefinition.defaultContainer ? ['A TaskDefinition must have at least one essential container'] : [], + }); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts index eba4ac4371ee8..3d8d113886709 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -1,6 +1,14 @@ -import { Resource, Tokenization } from '@aws-cdk/core'; +import { Tokenization } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { CommonTaskDefinitionProps, Compatibility, ITaskDefinition, NetworkMode, TaskDefinition } from '../base/task-definition'; +import { + CommonTaskDefinitionAttributes, + CommonTaskDefinitionProps, + Compatibility, + ITaskDefinition, + NetworkMode, + TaskDefinition, +} from '../base/task-definition'; +import { ImportedTaskDefinition } from '../base/_imported-task-definition'; /** * The properties for a task definition. @@ -51,6 +59,13 @@ export interface IFargateTaskDefinition extends ITaskDefinition { } +/** + * Attributes used to import an existing Fargate task definition + */ +export interface FargateTaskDefinitionAttributes extends CommonTaskDefinitionAttributes { + +} + /** * The details of a task definition run on a Fargate cluster. * @@ -62,14 +77,23 @@ export class FargateTaskDefinition extends TaskDefinition implements IFargateTas * Imports a task definition from the specified task definition ARN. */ public static fromFargateTaskDefinitionArn(scope: Construct, id: string, fargateTaskDefinitionArn: string): IFargateTaskDefinition { - class Import extends Resource implements IFargateTaskDefinition { - public readonly taskDefinitionArn = fargateTaskDefinitionArn; - public readonly compatibility = Compatibility.FARGATE; - public readonly isEc2Compatible = false; - public readonly isFargateCompatible = true; - } + return new ImportedTaskDefinition(scope, id, { taskDefinitionArn: fargateTaskDefinitionArn }); + } - return new Import(scope, id); + /** + * Import an existing Fargate task definition from its attributes + */ + public static fromFargateTaskDefinitionAttributes( + scope: Construct, + id: string, + attrs: FargateTaskDefinitionAttributes, + ): IFargateTaskDefinition { + return new ImportedTaskDefinition(scope, id, { + taskDefinitionArn: attrs.taskDefinitionArn, + compatibility: Compatibility.FARGATE, + networkMode: attrs.networkMode, + taskRole: attrs.taskRole, + }); } /** diff --git a/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts b/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts index e8dc339bf9e82..b9786f13e2816 100644 --- a/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts +++ b/packages/@aws-cdk/aws-ecs/lib/images/ecr.ts @@ -22,10 +22,14 @@ export class EcrImage extends ContainerImage { /** * Constructs a new instance of the EcrImage class. */ - constructor(private readonly repository: ecr.IRepository, private readonly tag: string) { + constructor(private readonly repository: ecr.IRepository, private readonly tagOrDigest: string) { super(); - this.imageName = this.repository.repositoryUriForTag(this.tag); + if (tagOrDigest?.startsWith('sha256:')) { + this.imageName = this.repository.repositoryUriForDigest(this.tagOrDigest); + } else { + this.imageName = this.repository.repositoryUriForTag(this.tagOrDigest); + } } public bind(_scope: CoreConstruct, containerDefinition: ContainerDefinition): ContainerImageConfig { diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index f7408e518f2fe..ec829127f9e84 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -73,6 +73,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3-deployment": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@types/nodeunit": "^0.0.31", "@types/proxyquire": "^1.3.28", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts index c167720e362cf..81e9f274160b7 100644 --- a/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/container-definition.test.ts @@ -6,6 +6,7 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as ssm from '@aws-cdk/aws-ssm'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as ecs from '../lib'; @@ -702,6 +703,55 @@ describe('container definition', () => { }); + test('can add port mappings to the container definition by props', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + taskDefinition.addContainer('cont', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + portMappings: [{ containerPort: 80 }], + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + PortMappings: [{ ContainerPort: 80 }], + }, + ], + }); + }); + + test('can add port mappings using props and addPortMappings and both are included', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + const containerDefinition = taskDefinition.addContainer('cont', { + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 1024, + portMappings: [{ containerPort: 80 }], + }); + + containerDefinition.addPortMappings({ containerPort: 443 }); + + // THEN + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + PortMappings: [ + { ContainerPort: 80 }, + { ContainerPort: 443 }, + ], + }, + ], + }); + }); + describe('Environment Files', () => { describe('with EC2 task definitions', () => { test('can add asset environment file to the container definition', () => { @@ -1643,7 +1693,7 @@ describe('container definition', () => { }); }); - testFutureBehavior('can use a DockerImageAsset directly for a container image', { '@aws-cdk/aws-ecr-assets:dockerIgnoreSupport': true }, cdk.App, (app) => { + testFutureBehavior('can use a DockerImageAsset directly for a container image', { [cxapi.DOCKER_IGNORE_SUPPORT]: true }, cdk.App, (app) => { // GIVEN const stack = new cdk.Stack(app, 'Stack'); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); 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 2279245aebaa4..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 @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elb from '@aws-cdk/aws-elasticloadbalancing'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; @@ -541,14 +541,48 @@ nodeunitShim({ const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + // Errors on validation, not on construction. + new ecs.Ec2Service(stack, 'Ec2Service', { + cluster, + taskDefinition, + }); + // THEN test.throws(() => { - new ecs.Ec2Service(stack, 'Ec2Service', { - cluster, - taskDefinition, - }); + expect(stack); + }, /one essential container/); + + test.done(); + }, + + 'allows adding the default container after creating the service'(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') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + new ecs.Ec2Service(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + // Add the container *after* creating the service + taskDefinition.addContainer('main', { + image: ecs.ContainerImage.fromRegistry('somecontainer'), + memoryReservationMiB: 10, }); + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Name: 'main', + }, + ], + })); + test.done(); }, @@ -2156,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/ec2-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts index bea2e3c0733e5..4f713b06d2d71 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-task-definition.test.ts @@ -6,6 +6,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as ssm from '@aws-cdk/aws-ssm'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as ecs from '../../lib'; @@ -530,6 +531,146 @@ describe('ec2 task definition', () => { }); + test('correctly sets containers from ECR repository using an image tag', () => { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage'), 'myTag'), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + ':myTag', + ], + ], + }, + Name: 'web', + }], + }); + }); + + test('correctly sets containers from ECR repository using an image digest', () => { + // GIVEN + const stack = new cdk.Stack(); + + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromEcrRepository(new Repository(stack, 'myECRImage'), 'sha256:94afd1f2e64d908bc90dbca0035a5b567EXAMPLE'), + memoryLimitMiB: 512, + }); + + // THEN + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [{ + Essential: true, + Memory: 512, + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'myECRImage7DEAE474', + 'Arn', + ], + }, + ], + }, + ], + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/', + { + Ref: 'myECRImage7DEAE474', + }, + '@sha256:94afd1f2e64d908bc90dbca0035a5b567EXAMPLE', + ], + ], + }, + Name: 'web', + }], + }); + }); + test('correctly sets containers from ECR repository using default props', () => { // GIVEN const stack = new cdk.Stack(); @@ -585,7 +726,7 @@ describe('ec2 task definition', () => { }); - testFutureBehavior('correctly sets containers from asset using default props', { '@aws-cdk/aws-ecr-assets:dockerIgnoreSupport': true }, cdk.App, (app) => { + testFutureBehavior('correctly sets containers from asset using default props', { [cxapi.DOCKER_IGNORE_SUPPORT]: true }, cdk.App, (app) => { // GIVEN const stack = new cdk.Stack(app, 'Stack'); @@ -1058,6 +1199,85 @@ describe('ec2 task definition', () => { }); }); + describe('When importing from an existing Ec2 TaskDefinition', () => { + test('can succeed using TaskDefinition Arn', () => { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + + // WHEN + const taskDefinition = ecs.Ec2TaskDefinition.fromEc2TaskDefinitionArn(stack, 'EC2_TD_ID', expectTaskDefinitionArn); + + // THEN + expect(taskDefinition.taskDefinitionArn).toBe(expectTaskDefinitionArn); + }); + }); + + describe('When importing from an existing Ec2 TaskDefinition using attributes', () => { + test('can set the imported task attribuets successfully', () => { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectNetworkMode = ecs.NetworkMode.AWS_VPC; + const expectTaskRole = new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + // WHEN + const taskDefinition = ecs.Ec2TaskDefinition.fromEc2TaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + networkMode: expectNetworkMode, + taskRole: expectTaskRole, + }); + + // THEN + expect(taskDefinition.taskDefinitionArn).toBe(expectTaskDefinitionArn); + expect(taskDefinition.compatibility).toBe(ecs.Compatibility.EC2); + expect(taskDefinition.isEc2Compatible).toBeTruthy(); + expect(taskDefinition.isFargateCompatible).toBeFalsy(); + expect(taskDefinition.networkMode).toBe(expectNetworkMode); + expect(taskDefinition.taskRole).toBe(expectTaskRole); + }); + + test('returns an Ec2 TaskDefinition that will throw an error when trying to access its yet to defined networkMode', () => { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectTaskRole = new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + // WHEN + const taskDefinition = ecs.Ec2TaskDefinition.fromEc2TaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + taskRole: expectTaskRole, + }); + + // THEN + expect(() => taskDefinition.networkMode).toThrow( + 'This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + + 'Add the \'networkMode\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + }); + + test('returns an Ec2 TaskDefinition that will throw an error when trying to access its yet to defined taskRole', () => { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectNetworkMode = ecs.NetworkMode.AWS_VPC; + + // WHEN + const taskDefinition = ecs.Ec2TaskDefinition.fromEc2TaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + networkMode: expectNetworkMode, + }); + + // THEN + expect(() => { taskDefinition.taskRole; }).toThrow( + 'This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + }); + }); + test('throws when setting proxyConfiguration without networkMode AWS_VPC', () => { // GIVEN const stack = new cdk.Stack(); @@ -1077,7 +1297,5 @@ describe('ec2 task definition', () => { expect(() => { new ecs.Ec2TaskDefinition(stack, 'TaskDef', { networkMode: ecs.NetworkMode.BRIDGE, proxyConfiguration }); }).toThrow(/ProxyConfiguration can only be used with AwsVpc network mode, got: bridge/); - - }); }); 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 acddf713d6d0a..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 @@ -482,14 +482,46 @@ nodeunitShim({ const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + // Errors on validation, not on construction. + new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + // THEN test.throws(() => { - new ecs.FargateService(stack, 'FargateService', { - cluster, - taskDefinition, - }); + expect(stack); + }, /one essential container/); + + test.done(); + }, + + 'allows adding the default container after creating the service'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + + new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + }); + + // Add the container *after* creating the service + taskDefinition.addContainer('main', { + image: ecs.ContainerImage.fromRegistry('somecontainer'), }); + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Name: 'main', + }, + ], + })); + test.done(); }, @@ -1802,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/fargate/fargate-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts index 00ec0028ef1a3..b8fad1bc9316f 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts @@ -5,7 +5,7 @@ import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; nodeunitShim({ - 'When creating an Fargate TaskDefinition': { + 'When creating a Fargate TaskDefinition': { 'with only required properties set, it correctly sets default properties'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -114,4 +114,90 @@ nodeunitShim({ test.done(); }, }, + + 'When importing from an existing Fargate TaskDefinition': { + 'can succeed using TaskDefinition Arn'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + + // WHEN + const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionArn(stack, 'FARGATE_TD_ID', expectTaskDefinitionArn); + + // THEN + test.equal(taskDefinition.taskDefinitionArn, expectTaskDefinitionArn); + test.done(); + }, + + 'can succeed using attributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectNetworkMode = ecs.NetworkMode.AWS_VPC; + const expectTaskRole = new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + // WHEN + const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + networkMode: expectNetworkMode, + taskRole: expectTaskRole, + }); + + // THEN + test.equal(taskDefinition.taskDefinitionArn, expectTaskDefinitionArn); + test.equal(taskDefinition.compatibility, ecs.Compatibility.FARGATE); + test.ok(taskDefinition.isFargateCompatible); + test.equal(taskDefinition.isEc2Compatible, false); + test.equal(taskDefinition.networkMode, expectNetworkMode); + test.equal(taskDefinition.taskRole, expectTaskRole); + + test.done(); + }, + + 'returns a Fargate TaskDefinition that will throw an error when trying to access its networkMode but its networkMode is undefined'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectTaskRole = new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + // WHEN + const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + taskRole: expectTaskRole, + }); + + // THEN + test.throws(() => { + taskDefinition.networkMode; + }, 'This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + + 'Add the \'networkMode\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + + test.done(); + }, + + 'returns a Fargate TaskDefinition that will throw an error when trying to access its taskRole but its taskRole is undefined'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectNetworkMode = ecs.NetworkMode.AWS_VPC; + + // WHEN + const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + networkMode: expectNetworkMode, + }); + + // THEN + test.throws(() => { + taskDefinition.taskRole; + }, 'This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + + test.done(); + }, + }, }); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts index d333fd9df89e2..e2d40e4ef52f6 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.ts @@ -15,13 +15,12 @@ const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', { cpu: 512, }); -const container = taskDefinition.addContainer('web', { +taskDefinition.addContainer('web', { image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), -}); - -container.addPortMappings({ - containerPort: 80, - protocol: ecs.Protocol.TCP, + portMappings: [{ + containerPort: 80, + protocol: ecs.Protocol.TCP, + }], }); const service = new ecs.FargateService(stack, 'Service', { diff --git a/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts index 34a7306106d66..709d3abd86026 100644 --- a/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts @@ -2,24 +2,121 @@ import { expect, haveResource } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; +import * as iam from '@aws-cdk/aws-iam'; nodeunitShim({ - 'A task definition with both compatibilities defaults to networkmode AwsVpc'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - new ecs.TaskDefinition(stack, 'TD', { - cpu: '512', - memoryMiB: '512', - compatibility: ecs.Compatibility.EC2_AND_FARGATE, - }); - - // THEN - expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { - NetworkMode: 'awsvpc', - })); - - test.done(); + 'When creating a new TaskDefinition': { + 'A task definition with both compatibilities defaults to networkmode AwsVpc'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.EC2_AND_FARGATE, + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + NetworkMode: 'awsvpc', + })); + + test.done(); + }, + }, + + 'When importing from an existing Task definition': { + 'can import using a task definition arn'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinitionArn = 'TDArn'; + + // WHEN + const taskDefinition = ecs.TaskDefinition.fromTaskDefinitionArn(stack, 'TD_ID', taskDefinitionArn); + + // THEN + test.equal(taskDefinition.taskDefinitionArn, taskDefinitionArn); + test.equal(taskDefinition.compatibility, ecs.Compatibility.EC2_AND_FARGATE); + test.equal(taskDefinition.executionRole, undefined); + + test.done(); + }, + + 'can import a Task Definition using attributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectCompatibility = ecs.Compatibility.EC2; + const expectNetworkMode = ecs.NetworkMode.AWS_VPC; + const expectTaskRole = new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + // WHEN + const taskDefinition = ecs.TaskDefinition.fromTaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + compatibility: expectCompatibility, + networkMode: expectNetworkMode, + taskRole: expectTaskRole, + }); + + // THEN + test.equal(taskDefinition.taskDefinitionArn, expectTaskDefinitionArn); + test.equal(taskDefinition.compatibility, expectCompatibility); + test.equal(taskDefinition.executionRole, undefined); + test.equal(taskDefinition.networkMode, expectNetworkMode); + test.equal(taskDefinition.taskRole, expectTaskRole); + + test.done(); + }, + + 'returns an imported TaskDefinition that will throw an error when trying to access its yet to defined networkMode'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectCompatibility = ecs.Compatibility.EC2; + const expectTaskRole = new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + // WHEN + const taskDefinition = ecs.TaskDefinition.fromTaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + compatibility: expectCompatibility, + taskRole: expectTaskRole, + }); + + // THEN + test.throws(() => { + taskDefinition.networkMode; + }, 'This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + + 'Add the \'networkMode\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + + test.done(); + }, + + 'returns an imported TaskDefinition that will throw an error when trying to access its yet to defined taskRole'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const expectTaskDefinitionArn = 'TD_ARN'; + const expectCompatibility = ecs.Compatibility.EC2; + const expectNetworkMode = ecs.NetworkMode.AWS_VPC; + + // WHEN + const taskDefinition = ecs.TaskDefinition.fromTaskDefinitionAttributes(stack, 'TD_ID', { + taskDefinitionArn: expectTaskDefinitionArn, + compatibility: expectCompatibility, + networkMode: expectNetworkMode, + }); + + // THEN + test.throws(() => { + taskDefinition.taskRole; + }, 'This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); + + test.done(); + }, }, }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index ac397ba62bd94..e0f3b1f4e5ea5 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -182,6 +182,9 @@ lb.addRedirect({ If you do not provide any options for this method, it redirects HTTP port 80 to HTTPS port 443. +By default all ingress traffic will be allowed on the source port. If you want to be more selective with your +ingress rules then set `open: false` and use the listener's `connections` object to selectively grant access to the listener. + ## Defining a Network Load Balancer Network Load Balancers are defined in a similar way to Application Load @@ -243,6 +246,33 @@ const group = listener.addTargets('AppFleet', { group.addTarget(asg2); ``` +### Sticky sessions for your Application Load Balancer + +By default, an Application Load Balancer routes each request independently to a registered target based on the chosen load-balancing algorithm. However, you can use the sticky session feature (also known as session affinity) to enable the load balancer to bind a user's session to a specific target. This ensures that all requests from the user during the session are sent to the same target. This feature is useful for servers that maintain state information in order to provide a continuous experience to clients. To use sticky sessions, the client must support cookies. + +Application Load Balancers support both duration-based cookies (`lb_cookie`) and application-based cookies (`app_cookie`). The key to managing sticky sessions is determining how long your load balancer should consistently route the user's request to the same target. Sticky sessions are enabled at the target group level. You can use a combination of duration-based stickiness, application-based stickiness, and no stickiness across all of your target groups. + +```ts +// Target group with duration-based stickiness with load-balancer generated cookie +const tg1 = new elbv2.ApplicationTargetGroup(stack, 'TG1', { + targetType: elbv2.TargetType.INSTANCE, + port: 80, + stickinessCookieDuration: cdk.Duration.minutes(5), + vpc, +}); + +// Target group with application-based stickiness +const tg2 = new elbv2.ApplicationTargetGroup(stack, 'TG2', { + targetType: elbv2.TargetType.INSTANCE, + port: 80, + stickinessCookieDuration: cdk.Duration.minutes(5), + stickinessCookieName: 'MyDeliciousCookie', + vpc, +}); +``` + +For more information see: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html#application-based-stickiness + ## Using Lambda Targets To use a Lambda Function as a target, use the integration class in the 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 1cd8a91c932aa..b72151f81f2f8 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -263,10 +263,14 @@ export class ApplicationListener extends BaseListener implements IApplicationLis this.certificateArns.push(first.certificateArn); } - if (additionalCerts.length > 0) { - new ApplicationListenerCertificate(this, id, { + // 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, { listener: this, - certificates: additionalCerts, + certificates: [additionalCerts[i]], }); } } @@ -363,6 +367,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis protocol: props.protocol, slowStart: props.slowStart, stickinessCookieDuration: props.stickinessCookieDuration, + stickinessCookieName: props.stickinessCookieName, targetGroupName: props.targetGroupName, targets: props.targets, vpc: this.loadBalancer.vpc, @@ -813,6 +818,20 @@ export interface AddApplicationTargetsProps extends AddRuleProps { */ readonly stickinessCookieDuration?: Duration; + /** + * The name of an application-based stickiness cookie. + * + * Names that start with the following prefixes are not allowed: AWSALB, AWSALBAPP, + * and AWSALBTG; they're reserved for use by the load balancer. + * + * Note: `stickinessCookieName` parameter depends on the presence of `stickinessCookieDuration` parameter. + * If `stickinessCookieDuration` is not set, `stickinessCookieName` will be omitted. + * + * @default - If `stickinessCookieDuration` is set, a load-balancer generated cookie is used. Otherwise, no stickiness is defined. + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html + */ + readonly stickinessCookieName?: string; + /** * The targets to add to this target group. * diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts index 8f0ccf963cc5b..4ad4dcb5fa081 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts @@ -119,7 +119,7 @@ export class ApplicationLoadBalancer extends BaseLoadBalancer implements IApplic return this.addListener(`Redirect${sourcePort}To${targetPort}`, { protocol: props.sourceProtocol ?? ApplicationProtocol.HTTP, port: sourcePort, - open: true, + open: props.open ?? true, defaultAction: ListenerAction.redirect({ port: targetPort, protocol: props.targetProtocol ?? ApplicationProtocol.HTTPS, @@ -665,4 +665,19 @@ export interface ApplicationLoadBalancerRedirectConfig { */ readonly targetPort?: number; + /** + * Allow anyone to connect to this listener + * + * If this is specified, the listener will be opened up to anyone who can reach it. + * For internal load balancers this is anyone in the same VPC. For public load + * balancers, this is anyone on the internet. + * + * If you want to be more selective about who can access this load + * balancer, set this to `false` and use the listener's `connections` + * object to selectively grant access to the listener. + * + * @default true + */ + readonly open?: boolean; + } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts index a1d3de25bf82d..74938a08ee745 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts @@ -1,6 +1,6 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; -import { Annotations, Duration } from '@aws-cdk/core'; +import { Annotations, Duration, Token } from '@aws-cdk/core'; import { IConstruct, Construct } from 'constructs'; import { ApplicationELBMetrics } from '../elasticloadbalancingv2-canned-metrics.generated'; import { @@ -57,6 +57,20 @@ export interface ApplicationTargetGroupProps extends BaseTargetGroupProps { */ readonly stickinessCookieDuration?: Duration; + /** + * The name of an application-based stickiness cookie. + * + * Names that start with the following prefixes are not allowed: AWSALB, AWSALBAPP, + * and AWSALBTG; they're reserved for use by the load balancer. + * + * Note: `stickinessCookieName` parameter depends on the presence of `stickinessCookieDuration` parameter. + * If `stickinessCookieDuration` is not set, `stickinessCookieName` will be omitted. + * + * @default - If `stickinessCookieDuration` is set, a load-balancer generated cookie is used. Otherwise, no stickiness is defined. + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html + */ + readonly stickinessCookieName?: string; + /** * The targets to add to this target group. * @@ -109,10 +123,13 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat if (props) { if (props.slowStart !== undefined) { + if (props.slowStart.toSeconds() < 30 || props.slowStart.toSeconds() > 900) { + throw new Error('Slow start duration value must be between 30 and 900 seconds.'); + } this.setAttribute('slow_start.duration_seconds', props.slowStart.toSeconds().toString()); } - if (props.stickinessCookieDuration !== undefined) { - this.enableCookieStickiness(props.stickinessCookieDuration); + if (props.stickinessCookieDuration) { + this.enableCookieStickiness(props.stickinessCookieDuration, props.stickinessCookieName); } this.addTarget(...(props.targets || [])); } @@ -129,12 +146,34 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat } /** - * Enable sticky routing via a cookie to members of this target group + * Enable sticky routing via a cookie to members of this target group. + * + * Note: If the `cookieName` parameter is set, application-based stickiness will be applied, + * otherwise it defaults to duration-based stickiness attributes (`lb_cookie`). + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html */ - public enableCookieStickiness(duration: Duration) { + public enableCookieStickiness(duration: Duration, cookieName?: string) { + if (duration.toSeconds() < 1 || duration.toSeconds() > 604800) { + throw new Error('Stickiness cookie duration value must be between 1 second and 7 days (604800 seconds).'); + } + if (cookieName !== undefined) { + if (!Token.isUnresolved(cookieName) && (cookieName.startsWith('AWSALB') || cookieName.startsWith('AWSALBAPP') || cookieName.startsWith('AWSALBTG'))) { + throw new Error('App cookie names that start with the following prefixes are not allowed: AWSALB, AWSALBAPP, and AWSALBTG; they\'re reserved for use by the load balancer.'); + } + if (cookieName === '') { + throw new Error('App cookie name cannot be an empty string.'); + } + } this.setAttribute('stickiness.enabled', 'true'); - this.setAttribute('stickiness.type', 'lb_cookie'); - this.setAttribute('stickiness.lb_cookie.duration_seconds', duration.toSeconds().toString()); + if (cookieName) { + this.setAttribute('stickiness.type', 'app_cookie'); + this.setAttribute('stickiness.app_cookie.cookie_name', cookieName); + this.setAttribute('stickiness.app_cookie.duration_seconds', duration.toSeconds().toString()); + } else { + this.setAttribute('stickiness.type', 'lb_cookie'); + this.setAttribute('stickiness.lb_cookie.duration_seconds', duration.toSeconds().toString()); + } } /** 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 b9de0961423ec..b6e379a17e463 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -71,7 +71,7 @@ describe('tests', () => { }); }); - test('Listener default to open - IPv4 and IPv6 (dualstack)', () => { + test('Listener default to open - IPv4 and IPv6 (dual stack)', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -143,6 +143,43 @@ describe('tests', () => { }); }); + test('HTTPS listener can add more than two certificates', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + + // WHEN + const listener = lb.addListener('Listener', { + port: 443, + defaultTargetGroups: [ + new elbv2.ApplicationTargetGroup(stack, 'Group', { vpc, port: 80 }), + ], + certificates: [ + elbv2.ListenerCertificate.fromArn('cert1'), + elbv2.ListenerCertificate.fromArn('cert2'), + elbv2.ListenerCertificate.fromArn('cert3'), + ], + }); + + expect(listener.node.tryFindChild('DefaultCertificates')).toBeDefined(); + expect(listener.node.tryFindChild('DefaultCertificates2')).toBeDefined(); + expect(listener.node.tryFindChild('DefaultCertificates3')).not.toBeDefined(); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener', { + Certificates: [{ CertificateArn: 'cert1' }], + }); + + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Certificates: [{ CertificateArn: 'cert2' }], + }); + + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Certificates: [{ CertificateArn: 'cert3' }], + }); + }); + test('Can configure targetType on TargetGroups', () => { // GIVEN const stack = new cdk.Stack(); @@ -316,7 +353,7 @@ describe('tests', () => { }); }); - test('Enable stickiness for targets', () => { + test('Enable alb stickiness for targets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -349,6 +386,43 @@ describe('tests', () => { }); }); + test('Enable app stickiness for targets', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + const listener = lb.addListener('Listener', { port: 80 }); + + // WHEN + const group = listener.addTargets('Group', { + port: 80, + targets: [new FakeSelfRegisteringTarget(stack, 'Target', vpc)], + }); + group.enableCookieStickiness(cdk.Duration.hours(1), 'MyDeliciousCookie'); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + TargetGroupAttributes: [ + { + Key: 'stickiness.enabled', + Value: 'true', + }, + { + Key: 'stickiness.type', + Value: 'app_cookie', + }, + { + Key: 'stickiness.app_cookie.cookie_name', + Value: 'MyDeliciousCookie', + }, + { + Key: 'stickiness.app_cookie.duration_seconds', + Value: '3600', + }, + ], + }); + }); + test('Enable health check for targets', () => { // GIVEN const stack = new cdk.Stack(); @@ -690,6 +764,31 @@ describe('tests', () => { }); }); + test('Can supress default ingress rules on a simple redirect response', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + + const loadBalancer = new elbv2.ApplicationLoadBalancer(stack, 'LB', { + vpc, + }); + + // WHEN + loadBalancer.addRedirect({ open: false }); + + // THEN + expect(stack).not.toHaveResourceLike('AWS::EC2::SecurityGroup', { + SecurityGroupIngress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow from anyone on port 80', + IpProtocol: 'tcp', + }, + ], + }); + + }); + test('Can add simple redirect responses with custom values', () => { // GIVEN const stack = new cdk.Stack(); @@ -823,7 +922,7 @@ describe('tests', () => { }); }); - test('Throws when specifying both target groups and fixed reponse', () => { + test('Throws when specifying both target groups and fixed response', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -868,7 +967,7 @@ describe('tests', () => { })).toThrowError('Priority must have value greater than or equal to 1'); }); - test('Throws when specifying both target groups and redirect reponse', () => { + test('Throws when specifying both target groups and redirect response', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -970,7 +1069,7 @@ describe('tests', () => { }); }); - test('Can add additional certificates via addCertficateArns to application listener', () => { + test('Can add additional certificates via addCertificateArns to application listener', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -1050,7 +1149,7 @@ describe('tests', () => { })).toThrowError('Both `pathPatterns` and `pathPattern` are specified, specify only one'); }); - test('Add additonal condition to listener rule', () => { + test('Add additional condition to listener rule', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -1244,7 +1343,7 @@ describe('tests', () => { }); }); - test('Can exist together legacy style conditions and modan style conditions', () => { + test('Can exist together legacy style conditions and modern style conditions', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts index 38b328ec153a4..f017a4a67f3fa 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts @@ -4,8 +4,12 @@ import { Metric } from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as elbv2 from '../../lib'; +const s3GrantWriteCtx = { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }; + describe('tests', () => { test('Trivial construction: internet facing', () => { // GIVEN @@ -122,9 +126,9 @@ describe('tests', () => { }); }); - test('Access logging', () => { + testFutureBehavior('Access logging', s3GrantWriteCtx, cdk.App, (app) => { // GIVEN - const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' } }); + const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); const vpc = new ec2.Vpc(stack, 'Stack'); const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); @@ -154,7 +158,7 @@ describe('tests', () => { Version: '2012-10-17', Statement: [ { - Action: ['s3:PutObject*', 's3:Abort*'], + Action: ['s3:PutObject', 's3:Abort*'], Effect: 'Allow', Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, Resource: { @@ -172,9 +176,9 @@ describe('tests', () => { }, ResourcePart.CompleteDefinition); }); - test('access logging with prefix', () => { + testFutureBehavior('access logging with prefix', s3GrantWriteCtx, cdk.App, (app) => { // GIVEN - const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' } }); + const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); const vpc = new ec2.Vpc(stack, 'Stack'); const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); @@ -207,7 +211,7 @@ describe('tests', () => { Version: '2012-10-17', Statement: [ { - Action: ['s3:PutObject*', 's3:Abort*'], + Action: ['s3:PutObject', 's3:Abort*'], Effect: 'Allow', Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, Resource: { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts index 1d8df0a706d9c..77858e9c21af4 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts @@ -88,4 +88,138 @@ describe('tests', () => { UnhealthyThresholdCount: 27, }); }); + + test('Load balancer duration cookie stickiness', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // WHEN + new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + stickinessCookieDuration: cdk.Duration.minutes(5), + vpc, + }); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + TargetGroupAttributes: [ + { + Key: 'stickiness.enabled', + Value: 'true', + }, + { + Key: 'stickiness.type', + Value: 'lb_cookie', + }, + { + Key: 'stickiness.lb_cookie.duration_seconds', + Value: '300', + }, + ], + }); + }); + + test('Load balancer app cookie stickiness', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // WHEN + new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + stickinessCookieDuration: cdk.Duration.minutes(5), + stickinessCookieName: 'MyDeliciousCookie', + vpc, + }); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + TargetGroupAttributes: [ + { + Key: 'stickiness.enabled', + Value: 'true', + }, + { + Key: 'stickiness.type', + Value: 'app_cookie', + }, + { + Key: 'stickiness.app_cookie.cookie_name', + Value: 'MyDeliciousCookie', + }, + { + Key: 'stickiness.app_cookie.duration_seconds', + Value: '300', + }, + ], + }); + }); + + test('Bad stickiness cookie names', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + const errMessage = 'App cookie names that start with the following prefixes are not allowed: AWSALB, AWSALBAPP, and AWSALBTG; they\'re reserved for use by the load balancer'; + + // THEN + ['AWSALBCookieName', 'AWSALBstickinessCookieName', 'AWSALBTGCookieName'].forEach((badCookieName, i) => { + expect(() => { + new elbv2.ApplicationTargetGroup(stack, `TargetGroup${i}`, { + stickinessCookieDuration: cdk.Duration.minutes(5), + stickinessCookieName: badCookieName, + vpc, + }); + }).toThrow(errMessage); + }); + }); + + test('Empty stickiness cookie name', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // THEN + expect(() => { + new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + stickinessCookieDuration: cdk.Duration.minutes(5), + stickinessCookieName: '', + vpc, + }); + }).toThrow(/App cookie name cannot be an empty string./); + }); + + test('Bad stickiness duration value', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // THEN + expect(() => { + new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + stickinessCookieDuration: cdk.Duration.days(8), + vpc, + }); + }).toThrow(/Stickiness cookie duration value must be between 1 second and 7 days \(604800 seconds\)./); + }); + + test('Bad slow start duration value', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // THEN + [cdk.Duration.minutes(16), cdk.Duration.seconds(29)].forEach((badDuration, i) => { + expect(() => { + new elbv2.ApplicationTargetGroup(stack, `TargetGroup${i}`, { + slowStart: badDuration, + vpc, + }); + }).toThrow(/Slow start duration value must be between 30 and 900 seconds./); + }); + }); }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.expected.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.expected.json index 8fd29b717c9d5..56acbde9bb1f1 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.expected.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.expected.json @@ -438,6 +438,20 @@ "Properties": { "Port": 80, "Protocol": "HTTP", + "TargetGroupAttributes": [ + { + "Key": "stickiness.enabled", + "Value": "true" + }, + { + "Key": "stickiness.type", + "Value": "lb_cookie" + }, + { + "Key": "stickiness.lb_cookie.duration_seconds", + "Value": "300" + } + ], "Targets": [ { "Id": "10.0.128.4" @@ -454,6 +468,28 @@ "Properties": { "Port": 80, "Protocol": "HTTP", + "TargetGroupAttributes": [ + { + "Key": "slow_start.duration_seconds", + "Value": "60" + }, + { + "Key": "stickiness.enabled", + "Value": "true" + }, + { + "Key": "stickiness.type", + "Value": "app_cookie" + }, + { + "Key": "stickiness.app_cookie.cookie_name", + "Value": "MyDeliciousCookie" + }, + { + "Key": "stickiness.app_cookie.duration_seconds", + "Value": "300" + } + ], "Targets": [ { "Id": "10.0.128.5" diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.ts index 251c730c2fa42..8643f7b4c1f69 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.ts @@ -22,6 +22,7 @@ const listener = lb.addListener('Listener', { const group1 = listener.addTargets('Target', { port: 80, targets: [new elbv2.IpTarget('10.0.128.4')], + stickinessCookieDuration: cdk.Duration.minutes(5), }); const group2 = listener.addTargets('ConditionalTarget', { @@ -29,6 +30,9 @@ const group2 = listener.addTargets('ConditionalTarget', { hostHeader: 'example.com', port: 80, targets: [new elbv2.IpTarget('10.0.128.5')], + stickinessCookieDuration: cdk.Duration.minutes(5), + stickinessCookieName: 'MyDeliciousCookie', + slowStart: cdk.Duration.minutes(1), }); group1.metricTargetResponseTime().createAlarm(stack, 'ResponseTimeHigh1', { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts index 88a3999ec0a6f..546b88ab7d541 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts @@ -3,8 +3,12 @@ import '@aws-cdk/assert/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as elbv2 from '../../lib'; +const s3GrantWriteCtx = { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }; + describe('tests', () => { test('Trivial construction: internet facing', () => { // GIVEN @@ -69,9 +73,9 @@ describe('tests', () => { }); }); - test('Access logging', () => { + testFutureBehavior('Access logging', s3GrantWriteCtx, cdk.App, (app) => { // GIVEN - const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' } }); + const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); const vpc = new ec2.Vpc(stack, 'Stack'); const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); @@ -101,7 +105,7 @@ describe('tests', () => { Version: '2012-10-17', Statement: [ { - Action: ['s3:PutObject*', 's3:Abort*'], + Action: ['s3:PutObject', 's3:Abort*'], Effect: 'Allow', Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, Resource: { @@ -137,9 +141,9 @@ describe('tests', () => { }, ResourcePart.CompleteDefinition); }); - test('access logging with prefix', () => { + testFutureBehavior('access logging with prefix', s3GrantWriteCtx, cdk.App, (app) => { // GIVEN - const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' } }); + const stack = new cdk.Stack(app, undefined, { env: { region: 'us-east-1' } }); const vpc = new ec2.Vpc(stack, 'Stack'); const bucket = new s3.Bucket(stack, 'AccessLoggingBucket'); const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); @@ -172,7 +176,7 @@ describe('tests', () => { Version: '2012-10-17', Statement: [ { - Action: ['s3:PutObject*', 's3:Abort*'], + Action: ['s3:PutObject', 's3:Abort*'], Effect: 'Allow', Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::127311923021:root']] } }, Resource: { diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index afd5bf6b45430..12810832c0da8 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -382,17 +382,37 @@ describe('log groups', () => { // Domain1 expect(stack).toHaveResourceLike('Custom::CloudwatchLogResourcePolicy', { Create: { - parameters: { - policyName: 'ESLogPolicyc836fd92f07ec41eb70c2f6f08dc4b43cfb7c25391', - }, + 'Fn::Join': [ + '', + [ + '{"service":"CloudWatchLogs","action":"putResourcePolicy","parameters":{"policyName":"ESLogPolicyc836fd92f07ec41eb70c2f6f08dc4b43cfb7c25391","policyDocument":"{\\"Statement\\":[{\\"Action\\":[\\"logs:PutLogEvents\\",\\"logs:CreateLogStream\\"],\\"Effect\\":\\"Allow\\",\\"Principal\\":{\\"Service\\":\\"es.amazonaws.com\\"},\\"Resource\\":\\"', + { + 'Fn::GetAtt': [ + 'Domain1AppLogs6E8D1D67', + 'Arn', + ], + }, + '\\"}],\\"Version\\":\\"2012-10-17\\"}"},"physicalResourceId":{"id":"ESLogGroupPolicyc836fd92f07ec41eb70c2f6f08dc4b43cfb7c25391"}}', + ], + ], }, }); // Domain2 expect(stack).toHaveResourceLike('Custom::CloudwatchLogResourcePolicy', { Create: { - parameters: { - policyName: 'ESLogPolicyc8f05f015be3baf6ec1ee06cd1ee5cc8706ebbe5b2', - }, + 'Fn::Join': [ + '', + [ + '{"service":"CloudWatchLogs","action":"putResourcePolicy","parameters":{"policyName":"ESLogPolicyc8f05f015be3baf6ec1ee06cd1ee5cc8706ebbe5b2","policyDocument":"{\\"Statement\\":[{\\"Action\\":[\\"logs:PutLogEvents\\",\\"logs:CreateLogStream\\"],\\"Effect\\":\\"Allow\\",\\"Principal\\":{\\"Service\\":\\"es.amazonaws.com\\"},\\"Resource\\":\\"', + { + 'Fn::GetAtt': [ + 'Domain2AppLogs810876E2', + 'Arn', + ], + }, + '\\"}],\\"Version\\":\\"2012-10-17\\"}"},"physicalResourceId":{"id":"ESLogGroupPolicyc8f05f015be3baf6ec1ee06cd1ee5cc8706ebbe5b2"}}', + ], + ], }, }); }); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts index 2b815c16b2048..8f19cef53553c 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/elasticsearch-access-policy.test.ts @@ -33,29 +33,25 @@ test('minimal example renders correctly', () => { 'Arn', ], }, - Create: { - service: 'ES', + Create: JSON.stringify({ action: 'updateElasticsearchDomainConfig', + service: 'ES', parameters: { DomainName: 'TestDomain', - AccessPolicies: '{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"test:arn\"}],\"Version\":\"2012-10-17\"}', + AccessPolicies: '{"Statement":[{"Action":"es:ESHttp*","Effect":"Allow","Principal":"*","Resource":"test:arn"}],"Version":"2012-10-17"}', }, outputPath: 'DomainConfig.ElasticsearchClusterConfig.AccessPolicies', - physicalResourceId: { - id: 'TestDomainAccessPolicy', - }, - }, - Update: { - service: 'ES', + physicalResourceId: { id: 'TestDomainAccessPolicy' }, + }), + Update: JSON.stringify({ action: 'updateElasticsearchDomainConfig', + service: 'ES', parameters: { DomainName: 'TestDomain', - AccessPolicies: '{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"test:arn\"}],\"Version\":\"2012-10-17\"}', + AccessPolicies: '{"Statement":[{"Action":"es:ESHttp*","Effect":"Allow","Principal":"*","Resource":"test:arn"}],"Version":"2012-10-17"}', }, outputPath: 'DomainConfig.ElasticsearchClusterConfig.AccessPolicies', - physicalResourceId: { - id: 'TestDomainAccessPolicy', - }, - }, + physicalResourceId: { id: 'TestDomainAccessPolicy' }, + }), }); }); diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json index 309d8c2831e8d..5424865cc3bd7 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.custom-kms-key.expected.json @@ -87,77 +87,50 @@ ] }, "Create": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", - { - "Fn::GetAtt": [ - "DomainSlowSearchLogs5B35A97A", - "Arn" - ] - }, - "\",\"", - { - "Fn::GetAtt": [ - "DomainAppLogs21698C1B", - "Arn" - ] - }, - "\"]}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "DomainSlowSearchLogs5B35A97A", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "ESLogGroupPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad" - } + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "DomainAppLogs21698C1B", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad\"}}" + ] + ] }, "Update": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", - { - "Fn::GetAtt": [ - "DomainSlowSearchLogs5B35A97A", - "Arn" - ] - }, - "\",\"", - { - "Fn::GetAtt": [ - "DomainAppLogs21698C1B", - "Arn" - ] - }, - "\"]}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "DomainSlowSearchLogs5B35A97A", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "ESLogGroupPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad" - } - }, - "Delete": { - "service": "CloudWatchLogs", - "action": "deleteResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad" - }, - "ignoreErrorCodesMatching": "400" + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "DomainAppLogs21698C1B", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad\"}}" + ] + ] }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc82ca7bfe2f2589b859ebab89e88da2efd284adfad\"},\"ignoreErrorCodesMatching\":\"400\"}", "InstallLatestAwsSdk": true }, "DependsOn": [ @@ -264,52 +237,36 @@ ] }, "Create": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain66AC69E0" - }, - "AccessPolicies": "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}" - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain66AC69E0" - }, - "AccessPolicy" - ] - ] - } - } + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain66AC69E0" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain66AC69E0" + }, + "AccessPolicy\"}}" + ] + ] }, "Update": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain66AC69E0" - }, - "AccessPolicies": "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}" - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain66AC69E0" - }, - "AccessPolicy" - ] - ] - } - } + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain66AC69E0" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain66AC69E0" + }, + "AccessPolicy\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -386,7 +343,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904" }, "S3Key": { "Fn::Join": [ @@ -399,7 +356,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -412,7 +369,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -439,17 +396,17 @@ } }, "Parameters": { - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904": { "Type": "String", - "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 bucket for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF": { "Type": "String", - "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 key for asset version \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343ArtifactHash0C561FF5": { "Type": "String", - "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "Artifact hash for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json index e510eb3534625..6d54b0cfbc27e 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.expected.json @@ -52,77 +52,50 @@ ] }, "Create": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", - { - "Fn::GetAtt": [ - "Domain1SlowSearchLogs8F3B0506", - "Arn" - ] - }, - "\",\"", - { - "Fn::GetAtt": [ - "Domain1AppLogs6E8D1D67", - "Arn" - ] - }, - "\"]}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "Domain1SlowSearchLogs8F3B0506", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6" - } + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "Domain1AppLogs6E8D1D67", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6\"}}" + ] + ] }, "Update": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", - { - "Fn::GetAtt": [ - "Domain1SlowSearchLogs8F3B0506", - "Arn" - ] - }, - "\",\"", - { - "Fn::GetAtt": [ - "Domain1AppLogs6E8D1D67", - "Arn" - ] - }, - "\"]}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "Domain1SlowSearchLogs8F3B0506", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6" - } - }, - "Delete": { - "service": "CloudWatchLogs", - "action": "deleteResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6" - }, - "ignoreErrorCodesMatching": "400" + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "Domain1AppLogs6E8D1D67", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6\"}}" + ] + ] }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc8858d5dba055f677469d76cb6ad538fd732ba69a6\"},\"ignoreErrorCodesMatching\":\"400\"}", "InstallLatestAwsSdk": true }, "DependsOn": [ @@ -226,52 +199,36 @@ ] }, "Create": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain19FCBCB91" - }, - "AccessPolicies": "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}" - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain19FCBCB91" - }, - "AccessPolicy" - ] - ] - } - } + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain19FCBCB91" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain19FCBCB91" + }, + "AccessPolicy\"}}" + ] + ] }, "Update": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain19FCBCB91" - }, - "AccessPolicies": "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}" - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain19FCBCB91" - }, - "AccessPolicy" - ] - ] - } - } + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain19FCBCB91" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain19FCBCB91" + }, + "AccessPolicy\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -318,7 +275,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904" }, "S3Key": { "Fn::Join": [ @@ -331,7 +288,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -344,7 +301,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -420,77 +377,50 @@ ] }, "Create": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", - { - "Fn::GetAtt": [ - "Domain2SlowSearchLogs0C75F64B", - "Arn" - ] - }, - "\",\"", - { - "Fn::GetAtt": [ - "Domain2AppLogs810876E2", - "Arn" - ] - }, - "\"]}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "Domain2SlowSearchLogs0C75F64B", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71" - } + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "Domain2AppLogs810876E2", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71\"}}" + ] + ] }, "Update": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"es.amazonaws.com\"},\"Resource\":[\"", - { - "Fn::GetAtt": [ - "Domain2SlowSearchLogs0C75F64B", - "Arn" - ] - }, - "\",\"", - { - "Fn::GetAtt": [ - "Domain2AppLogs810876E2", - "Arn" - ] - }, - "\"]}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"es.amazonaws.com\\\"},\\\"Resource\\\":[\\\"", + { + "Fn::GetAtt": [ + "Domain2SlowSearchLogs0C75F64B", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71" - } - }, - "Delete": { - "service": "CloudWatchLogs", - "action": "deleteResourcePolicy", - "parameters": { - "policyName": "ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71" - }, - "ignoreErrorCodesMatching": "400" + }, + "\\\",\\\"", + { + "Fn::GetAtt": [ + "Domain2AppLogs810876E2", + "Arn" + ] + }, + "\\\"]}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"ESLogGroupPolicyc8405238e455eeabd840cf6933e1814efc51d2de71\"}}" + ] + ] }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"ESLogPolicyc8405238e455eeabd840cf6933e1814efc51d2de71\"},\"ignoreErrorCodesMatching\":\"400\"}", "InstallLatestAwsSdk": true }, "DependsOn": [ @@ -594,52 +524,36 @@ ] }, "Create": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain2644FE48C" - }, - "AccessPolicies": "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}" - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain2644FE48C" - }, - "AccessPolicy" - ] - ] - } - } + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain2644FE48C" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain2644FE48C" + }, + "AccessPolicy\"}}" + ] + ] }, "Update": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain2644FE48C" - }, - "AccessPolicies": "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"*\"}],\"Version\":\"2012-10-17\"}" - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain2644FE48C" - }, - "AccessPolicy" - ] - ] - } - } + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain2644FE48C" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain2644FE48C" + }, + "AccessPolicy\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -652,17 +566,17 @@ } }, "Parameters": { - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904": { "Type": "String", - "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 bucket for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF": { "Type": "String", - "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 key for asset version \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343ArtifactHash0C561FF5": { "Type": "String", - "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "Artifact hash for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json index 065e569573421..89a79c79f8316 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json +++ b/packages/@aws-cdk/aws-elasticsearch/test/integ.elasticsearch.unsignedbasicauth.expected.json @@ -101,80 +101,50 @@ ] }, "Create": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain66AC69E0" - }, - "AccessPolicies": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"", - { - "Fn::GetAtt": [ - "Domain66AC69E0", - "Arn" - ] - }, - "/*\"}],\"Version\":\"2012-10-17\"}" - ] - ] - } - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain66AC69E0" - }, - "AccessPolicy" + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain66AC69E0" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "Domain66AC69E0", + "Arn" ] - ] - } - } + }, + "/*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain66AC69E0" + }, + "AccessPolicy\"}}" + ] + ] }, "Update": { - "action": "updateElasticsearchDomainConfig", - "service": "ES", - "parameters": { - "DomainName": { - "Ref": "Domain66AC69E0" - }, - "AccessPolicies": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":\"es:ESHttp*\",\"Effect\":\"Allow\",\"Principal\":\"*\",\"Resource\":\"", - { - "Fn::GetAtt": [ - "Domain66AC69E0", - "Arn" - ] - }, - "/*\"}],\"Version\":\"2012-10-17\"}" - ] - ] - } - }, - "outputPath": "DomainConfig.ElasticsearchClusterConfig.AccessPolicies", - "physicalResourceId": { - "id": { - "Fn::Join": [ - "", - [ - { - "Ref": "Domain66AC69E0" - }, - "AccessPolicy" + "Fn::Join": [ + "", + [ + "{\"action\":\"updateElasticsearchDomainConfig\",\"service\":\"ES\",\"parameters\":{\"DomainName\":\"", + { + "Ref": "Domain66AC69E0" + }, + "\",\"AccessPolicies\":\"{\\\"Statement\\\":[{\\\"Action\\\":\\\"es:ESHttp*\\\",\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "Domain66AC69E0", + "Arn" ] - ] - } - } + }, + "/*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"outputPath\":\"DomainConfig.ElasticsearchClusterConfig.AccessPolicies\",\"physicalResourceId\":{\"id\":\"", + { + "Ref": "Domain66AC69E0" + }, + "AccessPolicy\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -221,7 +191,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904" }, "S3Key": { "Fn::Join": [ @@ -234,7 +204,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -247,7 +217,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -273,17 +243,17 @@ } }, "Parameters": { - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904": { "Type": "String", - "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 bucket for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF": { "Type": "String", - "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 key for asset version \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343ArtifactHash0C561FF5": { "Type": "String", - "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "Artifact hash for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts index 4fefd1d13f8b6..f007f31f38c6d 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/log-group-resource-policy.test.ts @@ -31,7 +31,7 @@ test('minimal example renders correctly', () => { 'Arn', ], }, - Create: { + Create: JSON.stringify({ service: 'CloudWatchLogs', action: 'putResourcePolicy', parameters: { @@ -41,8 +41,8 @@ test('minimal example renders correctly', () => { physicalResourceId: { id: 'LogGroupResourcePolicy', }, - }, - Update: { + }), + Update: JSON.stringify({ service: 'CloudWatchLogs', action: 'putResourcePolicy', parameters: { @@ -52,14 +52,14 @@ test('minimal example renders correctly', () => { physicalResourceId: { id: 'LogGroupResourcePolicy', }, - }, - Delete: { + }), + Delete: JSON.stringify({ service: 'CloudWatchLogs', action: 'deleteResourcePolicy', parameters: { policyName: 'TestPolicy', }, ignoreErrorCodesMatching: '400', - }, + }), }); }); diff --git a/packages/@aws-cdk/aws-events-targets/README.md b/packages/@aws-cdk/aws-events-targets/README.md index 787bfcc433d30..cd843ef130a3a 100644 --- a/packages/@aws-cdk/aws-events-targets/README.md +++ b/packages/@aws-cdk/aws-events-targets/README.md @@ -27,11 +27,45 @@ Currently supported are: * Put a record to a Kinesis stream * Log an event into a LogGroup * Put a record to a Kinesis Data Firehose stream +* Put an event on an EventBridge bus See the README of the `@aws-cdk/aws-events` library for more information on EventBridge. -## LogGroup +## Invoke a Lambda function + +Use the `LambdaFunction` target to invoke a lambda function. + +The code snippet below creates an event rule with a Lambda function as a target +triggered for every events from `aws.ec2` source. You can optionally attach a +[dead letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html). + +```ts +import * as lambda from "@aws-cdk/aws-lambda"; +import * as events from "@aws-cdk/aws-events"; +import * as sqs from "@aws-cdk/aws-sqs"; +import * as targets from "@aws-cdk/aws-events-targets"; + +const fn = new lambda.Function(this, 'MyFunc', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.fromInline(`exports.handler = ${handler.toString()}`), +}); + +const rule = new events.Rule(this, 'rule', { + eventPattern: { + source: ["aws.ec2"], + }, +}); + +const queue = new sqs.Queue(this, 'Queue'); + +rule.addTarget(new targets.LambdaFunction(fn, { + deadLetterQueue: queue, // Optional: add a dead letter queue +})); +``` + +## Log an event into a LogGroup Use the `LogGroup` target to log your events in a CloudWatch LogGroup. diff --git a/packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts b/packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts index baff4c76cde66..a31fc0b68ca5c 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts @@ -18,7 +18,7 @@ export interface EcsTaskProps { /** * Task Definition of the task that should be started */ - readonly taskDefinition: ecs.TaskDefinition; + readonly taskDefinition: ecs.ITaskDefinition; /** * How many tasks should be started when this event is triggered @@ -103,7 +103,7 @@ export class EcsTask implements events.IRuleTarget { */ public readonly securityGroups?: ec2.ISecurityGroup[]; private readonly cluster: ecs.ICluster; - private readonly taskDefinition: ecs.TaskDefinition; + private readonly taskDefinition: ecs.ITaskDefinition; private readonly taskCount: number; private readonly role: iam.IRole; private readonly platformVersion?: ecs.FargatePlatformVersion; @@ -137,6 +137,13 @@ export class EcsTask implements events.IRuleTarget { this.securityGroups = props.securityGroups; return; } + + if (!cdk.Construct.isConstruct(this.taskDefinition)) { + throw new Error('Cannot create a security group for ECS task. ' + + 'The task definition in ECS task is not a Construct. ' + + 'Please pass a taskDefinition as a Construct in EcsTaskProps.'); + } + let securityGroup = props.securityGroup || this.taskDefinition.node.tryFindChild('SecurityGroup') as ec2.ISecurityGroup; securityGroup = securityGroup || new ec2.SecurityGroup(this.taskDefinition, 'SecurityGroup', { vpc: this.props.cluster.vpc }); this.securityGroup = securityGroup; // Maintain backwards-compatibility for customers that read the generated security group. diff --git a/packages/@aws-cdk/aws-events-targets/lib/event-bus.ts b/packages/@aws-cdk/aws-events-targets/lib/event-bus.ts new file mode 100644 index 0000000000000..1d07261a8eace --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/lib/event-bus.ts @@ -0,0 +1,45 @@ +import * as events from '@aws-cdk/aws-events'; +import * as iam from '@aws-cdk/aws-iam'; +import { singletonEventRole } from './util'; + +/** + * Configuration properties of an Event Bus event + */ +export interface EventBusProps { + /** + * Role to be used to publish the event + * + * @default a new role is created. + */ + readonly role?: iam.IRole; +} + +/** + * Notify an existing Event Bus of an event + */ +export class EventBus implements events.IRuleTarget { + private readonly role?: iam.IRole; + + constructor(private readonly eventBus: events.IEventBus, props: EventBusProps = {}) { + this.role = props.role; + } + + bind(rule: events.IRule, id?: string): events.RuleTargetConfig { + if (this.role) { + this.role.addToPrincipalPolicy(this.putEventStatement()); + } + const role = this.role ?? singletonEventRole(rule, [this.putEventStatement()]); + return { + id: id ?? '', + arn: this.eventBus.eventBusArn, + role, + }; + } + + private putEventStatement() { + return new iam.PolicyStatement({ + actions: ['events:PutEvents'], + resources: [this.eventBus.eventBusArn], + }); + } +} diff --git a/packages/@aws-cdk/aws-events-targets/lib/index.ts b/packages/@aws-cdk/aws-events-targets/lib/index.ts index bef8ce2463ffa..155791c195d1e 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/index.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/index.ts @@ -7,6 +7,7 @@ export * from './aws-api'; export * from './lambda'; export * from './ecs-task-properties'; export * from './ecs-task'; +export * from './event-bus'; export * from './state-machine'; export * from './kinesis-stream'; export * from './log-group'; diff --git a/packages/@aws-cdk/aws-events-targets/lib/lambda.ts b/packages/@aws-cdk/aws-events-targets/lib/lambda.ts index 780cd6d57162c..44315579a1300 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/lambda.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/lambda.ts @@ -1,6 +1,7 @@ import * as events from '@aws-cdk/aws-events'; import * as lambda from '@aws-cdk/aws-lambda'; -import { addLambdaPermission } from './util'; +import * as sqs from '@aws-cdk/aws-sqs'; +import { addLambdaPermission, addToDeadLetterQueueResourcePolicy } from './util'; /** * Customize the Lambda Event Target @@ -14,6 +15,18 @@ export interface LambdaFunctionProps { * @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; } /** @@ -32,9 +45,14 @@ export class LambdaFunction implements events.IRuleTarget { // Allow handler to be called from rule addLambdaPermission(rule, this.handler); + if (this.props.deadLetterQueue) { + addToDeadLetterQueueResourcePolicy(rule, this.props.deadLetterQueue); + } + return { id: '', arn: this.handler.functionArn, + deadLetterConfig: this.props.deadLetterQueue ? { arn: this.props.deadLetterQueue?.queueArn } : undefined, input: this.props.event, targetResource: this.handler, }; diff --git a/packages/@aws-cdk/aws-events-targets/lib/util.ts b/packages/@aws-cdk/aws-events-targets/lib/util.ts index 74465558bb3f9..069b04a8c5131 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/util.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/util.ts @@ -1,7 +1,8 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { ConstructNode, IConstruct, Names } from '@aws-cdk/core'; +import * as sqs from '@aws-cdk/aws-sqs'; +import { Annotations, ConstructNode, IConstruct, Names, Token, TokenComparison } 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 @@ -33,13 +34,14 @@ export function singletonEventRole(scope: IConstruct, policyStatements: iam.Poli export function addLambdaPermission(rule: events.IRule, handler: lambda.IFunction): void { let scope: Construct | undefined; let node: ConstructNode = handler.permissionsNode; + let permissionId = `AllowEventRule${Names.nodeUniqueId(rule.node)}`; if (rule instanceof Construct) { // Place the Permission resource in the same stack as Rule rather than the Function // This is to reduce circular dependency when the lambda handler and the rule are across stacks. scope = rule; node = rule.node; + permissionId = `AllowEventRule${Names.nodeUniqueId(handler.node)}`; } - const permissionId = `AllowEventRule${Names.nodeUniqueId(rule.node)}`; if (!node.tryFindChild(permissionId)) { handler.addPermission(permissionId, { scope, @@ -49,3 +51,45 @@ export function addLambdaPermission(rule: events.IRule, handler: lambda.IFunctio }); } } + +/** + * Allow a rule to send events with failed invocation to an Amazon SQS queue. + */ +export function addToDeadLetterQueueResourcePolicy(rule: events.IRule, queue: sqs.IQueue) { + if (!sameEnvDimension(rule.env.region, queue.env.region)) { + throw new Error(`Cannot assign Dead Letter Queue in region ${queue.env.region} to the rule ${Names.nodeUniqueId(rule.node)} in region ${rule.env.region}. Both the queue and the rule must be in the same region.`); + } + + // Skip Resource Policy creation if the Queue is not in the same account. + // There is no way to add a target onto an imported rule, so we can assume we will run the following code only + // in the account where the rule is created. + if (sameEnvDimension(rule.env.account, queue.env.account)) { + const policyStatementId = `AllowEventRule${Names.nodeUniqueId(rule.node)}`; + + queue.addToResourcePolicy(new iam.PolicyStatement({ + sid: policyStatementId, + principals: [new iam.ServicePrincipal('events.amazonaws.com')], + effect: iam.Effect.ALLOW, + actions: ['sqs:SendMessage'], + resources: [queue.queueArn], + conditions: { + ArnEquals: { + 'aws:SourceArn': rule.ruleArn, + }, + }, + })); + } else { + Annotations.of(rule).addWarning(`Cannot add a resource policy to your dead letter queue associated with rule ${rule.ruleName} because the queue is in a different account. You must add the resource policy manually to the dead letter queue in account ${queue.env.account}.`); + } +} + + +/** + * Whether two string probably contain the same environment dimension (region or account) + * + * Used to compare either accounts or regions, and also returns true if both + * are unresolved (in which case both are expted to be "current region" or "current account"). + */ +function sameEnvDimension(dim1: string, dim2: string) { + return [TokenComparison.SAME, TokenComparison.BOTH_UNRESOLVED].includes(Token.compareStrings(dim1, dim2)); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/aws-api/integ.aws-api.expected.json b/packages/@aws-cdk/aws-events-targets/test/aws-api/integ.aws-api.expected.json index 031c41c4e954c..f5ae531bc1a27 100644 --- a/packages/@aws-cdk/aws-events-targets/test/aws-api/integ.aws-api.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/aws-api/integ.aws-api.expected.json @@ -29,7 +29,26 @@ ] } }, - "ScheduleRuleAllowEventRuleawscdkawsapitargetintegScheduleRule51140722763E20C1": { + "ScheduleRuleAllowEventRuleawscdkawsapitargetintegScheduleRuleScheduleRuleTarget0HandlerF2C0C898874A4805": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "AWSb4cf1abd4e4f4bc699441af7ccd9ec371511E620", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "ScheduleRuleDA5BD877", + "Arn" + ] + } + } + }, + "ScheduleRuleAllowEventRuleawscdkawsapitargetintegScheduleRuleScheduleRuleTarget1Handler4688817C0179F894": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -115,7 +134,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342eS3Bucket38E36746" + "Ref": "AssetParameters6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94S3BucketEBC7D1F4" }, "S3Key": { "Fn::Join": [ @@ -128,7 +147,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342eS3VersionKeyFB07730C" + "Ref": "AssetParameters6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94S3VersionKeyDC9DCE00" } ] } @@ -141,7 +160,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342eS3VersionKeyFB07730C" + "Ref": "AssetParameters6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94S3VersionKeyDC9DCE00" } ] } @@ -151,13 +170,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "AWSb4cf1abd4e4f4bc699441af7ccd9ec37ServiceRole9FFE9C50", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs12.x" }, "DependsOn": [ @@ -198,7 +217,7 @@ ] } }, - "PatternRuleAllowEventRuleawscdkawsapitargetintegPatternRule3D388581AA4F776B": { + "PatternRuleAllowEventRuleawscdkawsapitargetintegPatternRulePatternRuleTarget0HandlerA0821464BB49C5D3": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -219,17 +238,17 @@ } }, "Parameters": { - "AssetParameters4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342eS3Bucket38E36746": { + "AssetParameters6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94S3BucketEBC7D1F4": { "Type": "String", - "Description": "S3 bucket for asset \"4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342e\"" + "Description": "S3 bucket for asset \"6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94\"" }, - "AssetParameters4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342eS3VersionKeyFB07730C": { + "AssetParameters6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94S3VersionKeyDC9DCE00": { "Type": "String", - "Description": "S3 key for asset version \"4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342e\"" + "Description": "S3 key for asset version \"6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94\"" }, - "AssetParameters4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342eArtifactHash3C551617": { + "AssetParameters6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94ArtifactHash34B6F705": { "Type": "String", - "Description": "Artifact hash for asset \"4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342e\"" + "Description": "Artifact hash for asset \"6e29dae9ab5b8a6c0aa8f922991cad784ebd048388ec6587d4832f307ef92f94\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json index 85af1bfde21c1..66418749eda33 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json @@ -41,6 +41,20 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "pipelinePipeline22F2A91DArtifactsBucketEncryptionKeyAlias9530209A": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/codepipeline-pipelineeventspipelinepipeline22f2a91dfbb66895", + "TargetKeyId": { + "Fn::GetAtt": [ + "pipelinePipeline22F2A91DArtifactsBucketEncryptionKey87C796D2", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "pipelinePipeline22F2A91DArtifactsBucketC1799DCD": { "Type": "AWS::S3::Bucket", "Properties": { @@ -69,20 +83,6 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, - "pipelinePipeline22F2A91DArtifactsBucketEncryptionKeyAlias9530209A": { - "Type": "AWS::KMS::Alias", - "Properties": { - "AliasName": "alias/codepipeline-pipelineeventspipelinepipeline22f2a91dfbb66895", - "TargetKeyId": { - "Fn::GetAtt": [ - "pipelinePipeline22F2A91DArtifactsBucketEncryptionKey87C796D2", - "Arn" - ] - } - }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete" - }, "pipelinePipeline22F2A91DRole58B7B05E": { "Type": "AWS::IAM::Role", "Properties": { @@ -439,4 +439,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/event-rule-target.test.ts b/packages/@aws-cdk/aws-events-targets/test/ecs/event-rule-target.test.ts index 4ac8347d02ec3..f0ab7770fadbd 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/event-rule-target.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/event-rule-target.test.ts @@ -58,6 +58,249 @@ test('Can use EC2 taskdef as EventRule target', () => { }); }); +test('Throws error for lacking of taskRole ' + + 'when importing from an EC2 task definition just from a task definition arn as EventRule target', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + }); + + const taskDefinition = ecs.Ec2TaskDefinition.fromEc2TaskDefinitionArn(stack, 'TaskDef', 'importedTaskDefArn'); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + // THEN + expect(() => { + rule.addTarget(new targets.EcsTask({ + cluster, + taskDefinition, + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', events.EventField.fromPath('$.detail.event')], + }], + })); + }).toThrow('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); +}); + +test('Can import an EC2 task definition from task definition attributes as EventRule target', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + }); + + const taskDefinition = ecs.Ec2TaskDefinition.fromEc2TaskDefinitionAttributes(stack, 'TaskDef', { + taskDefinitionArn: 'importedTaskDefArn', + networkMode: ecs.NetworkMode.BRIDGE, + taskRole: new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }), + }); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + // WHEN + rule.addTarget(new targets.EcsTask({ + cluster, + taskDefinition, + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', events.EventField.fromPath('$.detail.event')], + }], + })); + + // THEN + expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Targets: [ + { + Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, + EcsParameters: { + TaskCount: 1, + TaskDefinitionArn: 'importedTaskDefArn', + }, + InputTransformer: { + InputPathsMap: { + 'detail-event': '$.detail.event', + }, + InputTemplate: '{"containerOverrides":[{"name":"TheContainer","command":["echo",]}]}', + }, + RoleArn: { 'Fn::GetAtt': ['TaskDefEventsRoleFB3B67B8', 'Arn'] }, + Id: 'Target0', + }, + ], + }); +}); + +test('Throws error for lacking of taskRole ' + + 'when importing from a Fargate task definition just from a task definition arn as EventRule target', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionArn(stack, 'TaskDef', 'ImportedTaskDefArn'); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + // THEN + expect(() => { + rule.addTarget(new targets.EcsTask({ + cluster, + taskDefinition, + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', events.EventField.fromPath('$.detail.event')], + }], + })); + }).toThrow('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); +}); + +test('Can import a Fargate task definition from task definition attributes as EventRule target', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionAttributes(stack, 'TaskDef', { + taskDefinitionArn: 'importedTaskDefArn', + networkMode: ecs.NetworkMode.AWS_VPC, + taskRole: new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }), + }); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + // WHEN + rule.addTarget(new targets.EcsTask({ + cluster, + taskDefinition, + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', events.EventField.fromPath('$.detail.event')], + }], + })); + + // THEN + expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Targets: [ + { + Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, + EcsParameters: { + TaskCount: 1, + TaskDefinitionArn: 'importedTaskDefArn', + }, + InputTransformer: { + InputPathsMap: { + 'detail-event': '$.detail.event', + }, + InputTemplate: '{"containerOverrides":[{"name":"TheContainer","command":["echo",]}]}', + }, + RoleArn: { 'Fn::GetAtt': ['TaskDefEventsRoleFB3B67B8', 'Arn'] }, + Id: 'Target0', + }, + ], + }); +}); + +test('Throws error for lacking of taskRole ' + + 'when importing from a task definition just from a task definition arn as EventRule target', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + const taskDefinition = ecs.TaskDefinition.fromTaskDefinitionArn(stack, 'TaskDef', 'ImportedTaskDefArn'); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + // THEN + expect(() => { + rule.addTarget(new targets.EcsTask({ + cluster, + taskDefinition, + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', events.EventField.fromPath('$.detail.event')], + }], + })); + }).toThrow('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); +}); + +test('Can import a Task definition from task definition attributes as EventRule target', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + + const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionAttributes(stack, 'TaskDef', { + taskDefinitionArn: 'importedTaskDefArn', + networkMode: ecs.NetworkMode.AWS_VPC, + taskRole: new iam.Role(stack, 'TaskRole', { + assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), + }), + }); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + // WHEN + rule.addTarget(new targets.EcsTask({ + cluster, + taskDefinition, + taskCount: 1, + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', events.EventField.fromPath('$.detail.event')], + }], + })); + + // THEN + expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Targets: [ + { + Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, + EcsParameters: { + TaskCount: 1, + TaskDefinitionArn: 'importedTaskDefArn', + }, + InputTransformer: { + InputPathsMap: { + 'detail-event': '$.detail.event', + }, + InputTemplate: '{"containerOverrides":[{"name":"TheContainer","command":["echo",]}]}', + }, + RoleArn: { 'Fn::GetAtt': ['TaskDefEventsRoleFB3B67B8', 'Arn'] }, + Id: 'Target0', + }, + ], + }); +}); + test('Can use Fargate taskdef as EventRule target', () => { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json index eaa617893829d..6dc9c62e63102 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-ec2-task.lit.expected.json @@ -278,8 +278,6 @@ "ecs:Poll", "ecs:StartTelemetrySession" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -289,7 +287,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -474,8 +474,6 @@ "ecs:DescribeContainerInstances", "ecs:DescribeTasks" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -485,7 +483,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -536,14 +536,12 @@ "Code": { "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n while has_tasks(cluster, instance_arn):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\n\ndef has_tasks(cluster, instance_arn):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n tasks = instance['runningTasksCount'] + instance['pendingTasksCount']\n print('Instance %s has %s tasks' % (instance_arn, tasks))\n\n return tasks > 0\n\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" }, - "Handler": "index.lambda_handler", "Role": { "Fn::GetAtt": [ "EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA", "Arn" ] }, - "Runtime": "python3.6", "Environment": { "Variables": { "CLUSTER": { @@ -551,6 +549,8 @@ } } }, + "Handler": "index.lambda_handler", + "Runtime": "python3.6", "Tags": [ { "Key": "Name", @@ -935,4 +935,4 @@ "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-events-targets/test/ecs/integ.event-fargate-task.expected.json b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-fargate-task.expected.json index 8062d441cc13d..123578699510c 100644 --- a/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-fargate-task.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/ecs/integ.event-fargate-task.expected.json @@ -361,22 +361,6 @@ ] } }, - "TaskDefSecurityGroupD50E7CF0": { - "Type": "AWS::EC2::SecurityGroup", - "Properties": { - "GroupDescription": "aws-ecs-integ-fargate/TaskDef/SecurityGroup", - "SecurityGroupEgress": [ - { - "CidrIp": "0.0.0.0/0", - "Description": "Allow all outbound traffic by default", - "IpProtocol": "-1" - } - ], - "VpcId": { - "Ref": "Vpc8378EB38" - } - } - }, "TaskDefEventsRoleFB3B67B8": { "Type": "AWS::IAM::Role", "Properties": { @@ -447,6 +431,22 @@ ] } }, + "TaskDefSecurityGroupD50E7CF0": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ-fargate/TaskDef/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, "Rule4C995B7F": { "Type": "AWS::Events::Rule", "Properties": { @@ -498,4 +498,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/event-bus/event-rule-target.test.ts b/packages/@aws-cdk/aws-events-targets/test/event-bus/event-rule-target.test.ts new file mode 100644 index 0000000000000..4c39a907210d7 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/event-bus/event-rule-target.test.ts @@ -0,0 +1,93 @@ +import '@aws-cdk/assert/jest'; +import * as events from '@aws-cdk/aws-events'; +import * as iam from '@aws-cdk/aws-iam'; +import { Stack } from '@aws-cdk/core'; +import * as targets from '../../lib'; + +test('Use EventBus as an event rule target', () => { + const stack = new Stack(); + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + + rule.addTarget(new targets.EventBus(events.EventBus.fromEventBusArn( + stack, + 'External', + 'arn:aws:events:us-east-1:111111111111:default', + ), + )); + + expect(stack).toHaveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: 'arn:aws:events:us-east-1:111111111111:default', + Id: 'Target0', + RoleArn: { + 'Fn::GetAtt': [ + 'RuleEventsRoleC51A4248', + 'Arn', + ], + }, + }, + ], + }); + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Effect: 'Allow', + Action: 'events:PutEvents', + Resource: 'arn:aws:events:us-east-1:111111111111:default', + }], + Version: '2012-10-17', + }, + Roles: [{ + Ref: 'RuleEventsRoleC51A4248', + }], + }); +}); + +test('with supplied role', () => { + const stack = new Stack(); + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), + roleName: 'GivenRole', + }); + + rule.addTarget(new targets.EventBus( + events.EventBus.fromEventBusArn( + stack, + 'External', + 'arn:aws:events:us-east-1:123456789012:default', + ), + { role }, + )); + + expect(stack).toHaveResource('AWS::Events::Rule', { + Targets: [{ + Arn: 'arn:aws:events:us-east-1:123456789012:default', + Id: 'Target0', + RoleArn: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], + }, + }], + }); + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Effect: 'Allow', + Action: 'events:PutEvents', + Resource: 'arn:aws:events:us-east-1:123456789012:default', + }], + Version: '2012-10-17', + }, + Roles: [{ + Ref: 'Role1ABCC5F0', + }], + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.expected.json b/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.expected.json new file mode 100644 index 0000000000000..632ddf1767598 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.expected.json @@ -0,0 +1,83 @@ +{ + "Resources": { + "Rule4C995B7F": { + "Type": "AWS::Events::Rule", + "Properties": { + "ScheduleExpression": "rate(1 minute)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::Join": [ + "", + [ + "arn:aws:events:", + { + "Ref": "AWS::Region" + }, + ":999999999999:event-bus/test-bus" + ] + ] + }, + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "RuleEventsRoleC51A4248", + "Arn" + ] + } + } + ] + } + }, + "RuleEventsRoleC51A4248": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "RuleEventsRoleDefaultPolicy0510525D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "events:PutEvents", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:events:", + { + "Ref": "AWS::Region" + }, + ":999999999999:event-bus/test-bus" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RuleEventsRoleDefaultPolicy0510525D", + "Roles": [ + { + "Ref": "RuleEventsRoleC51A4248" + } + ] + } + } + } +} diff --git a/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.ts b/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.ts new file mode 100644 index 0000000000000..c0ec2ea421b85 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/event-bus/integ.event-bus.ts @@ -0,0 +1,26 @@ +/// !cdk-integ pragma:ignore-assets +import * as events from '@aws-cdk/aws-events'; +import * as cdk from '@aws-cdk/core'; +import * as targets from '../../lib'; + +const app = new cdk.App(); + +class EventSourceStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const rule = new events.Rule(this, 'Rule', { + schedule: events.Schedule.expression('rate(1 minute)'), + }); + rule.addTarget(new targets.EventBus( + events.EventBus.fromEventBusArn( + this, + 'External', + `arn:aws:events:${this.region}:999999999999:event-bus/test-bus`, + ), + )); + } +} + +new EventSourceStack(app, 'event-source-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json index 460d13d03e0ca..4151890c35ef3 100644 --- a/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json @@ -1,89 +1,89 @@ { - "Resources":{ - "MyStream5C050E93":{ - "Type":"AWS::Kinesis::Stream", - "Properties":{ - "ShardCount":1, - "RetentionPeriodHours":24, - "StreamEncryption":{ - "Fn::If":[ + "Resources": { + "MyStream5C050E93": { + "Type": "AWS::Kinesis::Stream", + "Properties": { + "ShardCount": 1, + "RetentionPeriodHours": 24, + "StreamEncryption": { + "Fn::If": [ "AwsCdkKinesisEncryptedStreamsUnsupportedRegions", { - "Ref":"AWS::NoValue" + "Ref": "AWS::NoValue" }, { - "EncryptionType":"KMS", - "KeyId":"alias/aws/kinesis" + "EncryptionType": "KMS", + "KeyId": "alias/aws/kinesis" } ] } } }, - "MyStreamEventsRole5B6CC6AF":{ - "Type":"AWS::IAM::Role", - "Properties":{ - "AssumeRolePolicyDocument":{ - "Statement":[ + "MyStreamEventsRole5B6CC6AF": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ { - "Action":"sts:AssumeRole", - "Effect":"Allow", - "Principal":{ - "Service":"events.amazonaws.com" + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" } } ], - "Version":"2012-10-17" + "Version": "2012-10-17" } } }, - "MyStreamEventsRoleDefaultPolicy2089B49E":{ - "Type":"AWS::IAM::Policy", - "Properties":{ - "PolicyDocument":{ - "Statement":[ + "MyStreamEventsRoleDefaultPolicy2089B49E": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ { - "Action":[ + "Action": [ "kinesis:PutRecord", "kinesis:PutRecords" ], - "Effect":"Allow", - "Resource":{ - "Fn::GetAtt":[ + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ "MyStream5C050E93", "Arn" ] } } ], - "Version":"2012-10-17" + "Version": "2012-10-17" }, - "PolicyName":"MyStreamEventsRoleDefaultPolicy2089B49E", - "Roles":[ + "PolicyName": "MyStreamEventsRoleDefaultPolicy2089B49E", + "Roles": [ { - "Ref":"MyStreamEventsRole5B6CC6AF" + "Ref": "MyStreamEventsRole5B6CC6AF" } ] } }, - "EveryMinute2BBCEA8F":{ - "Type":"AWS::Events::Rule", - "Properties":{ - "ScheduleExpression":"rate(1 minute)", - "State":"ENABLED", - "Targets":[ + "EveryMinute2BBCEA8F": { + "Type": "AWS::Events::Rule", + "Properties": { + "ScheduleExpression": "rate(1 minute)", + "State": "ENABLED", + "Targets": [ { - "Arn":{ - "Fn::GetAtt":[ + "Arn": { + "Fn::GetAtt": [ "MyStream5C050E93", "Arn" ] }, - "Id":"Target0", - "KinesisParameters":{ - "PartitionKeyPath":"$.id" + "Id": "Target0", + "KinesisParameters": { + "PartitionKeyPath": "$.id" }, - "RoleArn":{ - "Fn::GetAtt":[ + "RoleArn": { + "Fn::GetAtt": [ "MyStreamEventsRole5B6CC6AF", "Arn" ] @@ -93,21 +93,21 @@ } } }, - "Conditions":{ - "AwsCdkKinesisEncryptedStreamsUnsupportedRegions":{ - "Fn::Or":[ + "Conditions": { + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions": { + "Fn::Or": [ { - "Fn::Equals":[ + "Fn::Equals": [ { - "Ref":"AWS::Region" + "Ref": "AWS::Region" }, "cn-north-1" ] }, { - "Fn::Equals":[ + "Fn::Equals": [ { - "Ref":"AWS::Region" + "Ref": "AWS::Region" }, "cn-northwest-1" ] diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json index aad7c05f0bd5f..7df2a4becf021 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json @@ -37,13 +37,13 @@ "Code": { "ZipFile": "exports.handler = function handler(event, _context, callback) {\n console.log(JSON.stringify(event, undefined, 2));\n return callback();\n}" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "MyFuncServiceRole54065130", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -68,7 +68,7 @@ ] } }, - "TimerAllowEventRulelambdaeventsTimer0E6AB6D890F582F4": { + "TimerAllowEventRulelambdaeventsMyFunc910E580F793D7BBB": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -105,7 +105,7 @@ ] } }, - "Timer2AllowEventRulelambdaeventsTimer27F866A1E50659689": { + "Timer2AllowEventRulelambdaeventsMyFunc910E580FCCD9CDCE": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -123,6 +123,95 @@ ] } } + }, + "Timer30894E3BB": { + "Type": "AWS::Events::Rule", + "Properties": { + "ScheduleExpression": "rate(2 minutes)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "MyFunc8A243A2C", + "Arn" + ] + }, + "DeadLetterConfig": { + "Arn": { + "Fn::GetAtt": [ + "Queue4A7E3555", + "Arn" + ] + } + }, + "Id": "Target0" + } + ] + } + }, + "Timer3AllowEventRulelambdaeventsMyFunc910E580F79317F73": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyFunc8A243A2C", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "Timer30894E3BB", + "Arn" + ] + } + } + }, + "Queue4A7E3555": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "QueuePolicy25439813": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Fn::GetAtt": [ + "Timer30894E3BB", + "Arn" + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "events.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "Queue4A7E3555", + "Arn" + ] + }, + "Sid": "AllowEventRulelambdaeventsTimer3107B9373" + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "Queue4A7E3555" + } + ] + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts index 17d17a291e244..c37c632d803ae 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts @@ -1,5 +1,6 @@ import * as events from '@aws-cdk/aws-events'; import * as lambda from '@aws-cdk/aws-lambda'; +import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; import * as targets from '../../lib'; @@ -23,6 +24,17 @@ const timer2 = new events.Rule(stack, 'Timer2', { }); timer2.addTarget(new targets.LambdaFunction(fn)); + +const timer3 = new events.Rule(stack, 'Timer3', { + schedule: events.Schedule.rate(cdk.Duration.minutes(2)), +}); + +const queue = new sqs.Queue(stack, 'Queue'); + +timer3.addTarget(new targets.LambdaFunction(fn, { + deadLetterQueue: queue, +})); + app.synth(); /* eslint-disable no-console */ diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts b/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts index 28df0932c9ba4..c5fd260715293 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts @@ -1,6 +1,7 @@ import '@aws-cdk/assert/jest'; import * as events from '@aws-cdk/aws-events'; import * as lambda from '@aws-cdk/aws-lambda'; +import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; import * as targets from '../../lib'; @@ -78,6 +79,27 @@ test('adding same lambda function as target mutiple times creates permission onl expect(stack).toCountResources('AWS::Lambda::Permission', 1); }); +test('adding different lambda functions as target mutiple times creates multiple permissions', () => { + // GIVEN + const stack = new cdk.Stack(); + const fn1 = newTestLambda(stack); + const fn2 = newTestLambda(stack, '2'); + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + // WHEN + rule.addTarget(new targets.LambdaFunction(fn1, { + event: events.RuleTargetInput.fromObject({ key: 'value1' }), + })); + rule.addTarget(new targets.LambdaFunction(fn2, { + event: events.RuleTargetInput.fromObject({ key: 'value2' }), + })); + + // THEN + expect(stack).toCountResources('AWS::Lambda::Permission', 2); +}); + test('adding same singleton lambda function as target mutiple times creates permission only once', () => { // GIVEN const stack = new cdk.Stack(); @@ -126,8 +148,185 @@ test('lambda handler and cloudwatch event across stacks', () => { expect(eventStack).toCountResources('AWS::Lambda::Permission', 1); }); -function newTestLambda(scope: constructs.Construct) { - return new lambda.Function(scope, 'MyLambda', { +test('use a Dead Letter Queue for the rule target', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + + const fn = new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'bar', + runtime: lambda.Runtime.PYTHON_2_7, + }); + + const queue = new sqs.Queue(stack, 'Queue'); + + new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + targets: [new targets.LambdaFunction(fn, { + deadLetterQueue: queue, + })], + }); + + expect(() => app.synth()).not.toThrow(); + + // the Permission resource should be in the event stack + expect(stack).toHaveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: { + 'Fn::GetAtt': [ + 'MyLambdaCCE802FB', + 'Arn', + ], + }, + DeadLetterConfig: { + Arn: { + 'Fn::GetAtt': [ + 'Queue4A7E3555', + 'Arn', + ], + }, + }, + Id: 'Target0', + }, + ], + }); + + 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': [ + 'Queue4A7E3555', + 'Arn', + ], + }, + Sid: 'AllowEventRuleStackRuleF6E31DD0', + }, + ], + Version: '2012-10-17', + }, + Queues: [ + { + Ref: 'Queue4A7E3555', + }, + ], + }); +}); + +test('throw an error when using a Dead Letter Queue for the rule target in a different region', () => { + // GIVEN + const app = new cdk.App(); + const stack1 = new cdk.Stack(app, 'Stack1', { + env: { + region: 'eu-west-1', + }, + }); + const stack2 = new cdk.Stack(app, 'Stack2', { + env: { + region: 'eu-west-2', + }, + }); + + const fn = new lambda.Function(stack1, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'bar', + runtime: lambda.Runtime.PYTHON_2_7, + }); + + const queue = new sqs.Queue(stack2, 'Queue'); + + let rule = new events.Rule(stack1, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + + expect(() => { + rule.addTarget(new targets.LambdaFunction(fn, { + deadLetterQueue: queue, + })); + }).toThrow(/Cannot assign Dead Letter Queue in region eu-west-2 to the rule Stack1Rule92BA1111 in region eu-west-1. Both the queue and the rule must be in the same region./); +}); + +test('must display a warning when using a Dead Letter Queue from another account', () => { + // GIVEN + const app = new cdk.App(); + const stack1 = new cdk.Stack(app, 'Stack1', { + env: { + region: 'eu-west-1', + account: '111111111111', + }, + }); + + const stack2 = new cdk.Stack(app, 'Stack2', { + env: { + region: 'eu-west-1', + account: '222222222222', + }, + }); + + const fn = new lambda.Function(stack1, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'bar', + runtime: lambda.Runtime.PYTHON_2_7, + }); + + const queue = sqs.Queue.fromQueueArn(stack2, 'Queue', 'arn:aws:sqs:eu-west-1:444455556666:queue1'); + + new events.Rule(stack1, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + targets: [new targets.LambdaFunction(fn, { + deadLetterQueue: queue, + })], + }); + + expect(() => app.synth()).not.toThrow(); + + // the Permission resource should be in the event stack + expect(stack1).toHaveResource('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 minute)', + State: 'ENABLED', + Targets: [ + { + Arn: { + 'Fn::GetAtt': [ + 'MyLambdaCCE802FB', + 'Arn', + ], + }, + DeadLetterConfig: { + Arn: 'arn:aws:sqs:eu-west-1:444455556666:queue1', + }, + Id: 'Target0', + }, + ], + }); + + expect(stack1).not.toHaveResource('AWS::SQS::QueuePolicy'); + + let rule = stack1.node.children.find(child => child instanceof events.Rule); + expect(rule?.node.metadata[0].data).toMatch(/Cannot add a resource policy to your dead letter queue associated with rule .* because the queue is in a different account\. You must add the resource policy manually to the dead letter queue in account 222222222222\./); +}); + +function newTestLambda(scope: constructs.Construct, suffix = '') { + return new lambda.Function(scope, `MyLambda${suffix}`, { code: new lambda.InlineCode('foo'), handler: 'bar', runtime: lambda.Runtime.PYTHON_2_7, diff --git a/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.expected.json b/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.expected.json index 1f4e086b51607..9a150d9cafa32 100644 --- a/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.expected.json @@ -98,63 +98,36 @@ ] }, "Create": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimerC63340B025F606BE", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Resource\":\"", - { - "Fn::GetAtt": [ - "loggroupB02AAEB1", - "Arn" - ] - }, - "\"}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimerC63340B025F606BE\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"events.amazonaws.com\\\"},\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "loggroupB02AAEB1", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "EventsLogGroupPolicyloggroupeventsTimerC63340B0" - } + }, + "\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"EventsLogGroupPolicyloggroupeventsTimerC63340B0\"}}" + ] + ] }, "Update": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimerC63340B025F606BE", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Resource\":\"", - { - "Fn::GetAtt": [ - "loggroupB02AAEB1", - "Arn" - ] - }, - "\"}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimerC63340B025F606BE\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"events.amazonaws.com\\\"},\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "loggroupB02AAEB1", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "EventsLogGroupPolicyloggroupeventsTimerC63340B0" - } - }, - "Delete": { - "service": "CloudWatchLogs", - "action": "deleteResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimerC63340B025F606BE" - }, - "ignoreErrorCodesMatching": "400" + }, + "\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"EventsLogGroupPolicyloggroupeventsTimerC63340B0\"}}" + ] + ] }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimerC63340B025F606BE\"},\"ignoreErrorCodesMatching\":\"400\"}", "InstallLatestAwsSdk": true }, "DependsOn": [ @@ -199,7 +172,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904" }, "S3Key": { "Fn::Join": [ @@ -212,7 +185,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -225,7 +198,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -235,13 +208,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs12.x", "Timeout": 120 }, @@ -326,63 +299,36 @@ ] }, "Create": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimer289E3527EF8F6205F", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Resource\":\"", - { - "Fn::GetAtt": [ - "loggroup2F19C5C9B", - "Arn" - ] - }, - "\"}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimer289E3527EF8F6205F\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"events.amazonaws.com\\\"},\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "loggroup2F19C5C9B", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "EventsLogGroupPolicyloggroupeventsTimer289E3527E" - } + }, + "\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"EventsLogGroupPolicyloggroupeventsTimer289E3527E\"}}" + ] + ] }, "Update": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimer289E3527EF8F6205F", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Resource\":\"", - { - "Fn::GetAtt": [ - "loggroup2F19C5C9B", - "Arn" - ] - }, - "\"}],\"Version\":\"2012-10-17\"}" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimer289E3527EF8F6205F\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"events.amazonaws.com\\\"},\\\"Resource\\\":\\\"", + { + "Fn::GetAtt": [ + "loggroup2F19C5C9B", + "Arn" ] - ] - } - }, - "physicalResourceId": { - "id": "EventsLogGroupPolicyloggroupeventsTimer289E3527E" - } - }, - "Delete": { - "service": "CloudWatchLogs", - "action": "deleteResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimer289E3527EF8F6205F" - }, - "ignoreErrorCodesMatching": "400" + }, + "\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"EventsLogGroupPolicyloggroupeventsTimer289E3527E\"}}" + ] + ] }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimer289E3527EF8F6205F\"},\"ignoreErrorCodesMatching\":\"400\"}", "InstallLatestAwsSdk": true }, "DependsOn": [ @@ -459,73 +405,46 @@ ] }, "Create": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimer37DF74C17EF314A8E", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Resource\":\"arn:", - { - "Ref": "AWS::Partition" - }, - ":logs:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":log-group:MyLogGroupNameToBeImported:*\"}],\"Version\":\"2012-10-17\"}" - ] - ] - } - }, - "physicalResourceId": { - "id": "EventsLogGroupPolicyloggroupeventsTimer37DF74C17" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimer37DF74C17EF314A8E\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"events.amazonaws.com\\\"},\\\"Resource\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:MyLogGroupNameToBeImported:*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"EventsLogGroupPolicyloggroupeventsTimer37DF74C17\"}}" + ] + ] }, "Update": { - "service": "CloudWatchLogs", - "action": "putResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimer37DF74C17EF314A8E", - "policyDocument": { - "Fn::Join": [ - "", - [ - "{\"Statement\":[{\"Action\":[\"logs:PutLogEvents\",\"logs:CreateLogStream\"],\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"events.amazonaws.com\"},\"Resource\":\"arn:", - { - "Ref": "AWS::Partition" - }, - ":logs:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":log-group:MyLogGroupNameToBeImported:*\"}],\"Version\":\"2012-10-17\"}" - ] - ] - } - }, - "physicalResourceId": { - "id": "EventsLogGroupPolicyloggroupeventsTimer37DF74C17" - } - }, - "Delete": { - "service": "CloudWatchLogs", - "action": "deleteResourcePolicy", - "parameters": { - "policyName": "loggroupeventsEventsLogGroupPolicyloggroupeventsTimer37DF74C17EF314A8E" - }, - "ignoreErrorCodesMatching": "400" + "Fn::Join": [ + "", + [ + "{\"service\":\"CloudWatchLogs\",\"action\":\"putResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimer37DF74C17EF314A8E\",\"policyDocument\":\"{\\\"Statement\\\":[{\\\"Action\\\":[\\\"logs:PutLogEvents\\\",\\\"logs:CreateLogStream\\\"],\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":{\\\"Service\\\":\\\"events.amazonaws.com\\\"},\\\"Resource\\\":\\\"arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:MyLogGroupNameToBeImported:*\\\"}],\\\"Version\\\":\\\"2012-10-17\\\"}\"},\"physicalResourceId\":{\"id\":\"EventsLogGroupPolicyloggroupeventsTimer37DF74C17\"}}" + ] + ] }, + "Delete": "{\"service\":\"CloudWatchLogs\",\"action\":\"deleteResourcePolicy\",\"parameters\":{\"policyName\":\"loggroupeventsEventsLogGroupPolicyloggroupeventsTimer37DF74C17EF314A8E\"},\"ignoreErrorCodesMatching\":\"400\"}", "InstallLatestAwsSdk": true }, "DependsOn": [ @@ -536,17 +455,17 @@ } }, "Parameters": { - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904": { "Type": "String", - "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 bucket for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF": { "Type": "String", - "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 key for asset version \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343ArtifactHash0C561FF5": { "Type": "String", - "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "Artifact hash for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/logs/log-group-resource-policy.test.ts b/packages/@aws-cdk/aws-events-targets/test/logs/log-group-resource-policy.test.ts index 999d813105baf..e60c4b84be8bb 100644 --- a/packages/@aws-cdk/aws-events-targets/test/logs/log-group-resource-policy.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/logs/log-group-resource-policy.test.ts @@ -31,35 +31,8 @@ test('minimal example renders correctly', () => { 'Arn', ], }, - Create: { - service: 'CloudWatchLogs', - action: 'putResourcePolicy', - parameters: { - policyName: 'TestPolicy', - policyDocument: '{"Statement":[{"Action":["logs:PutLogEvents","logs:CreateLogStream"],"Effect":"Allow","Principal":{"Service":"es.amazonaws.com"},"Resource":"*"}],"Version":"2012-10-17"}', - }, - physicalResourceId: { - id: 'LogGroupResourcePolicy', - }, - }, - Update: { - service: 'CloudWatchLogs', - action: 'putResourcePolicy', - parameters: { - policyName: 'TestPolicy', - policyDocument: '{"Statement":[{"Action":["logs:PutLogEvents","logs:CreateLogStream"],"Effect":"Allow","Principal":{"Service":"es.amazonaws.com"},"Resource":"*"}],"Version":"2012-10-17"}', - }, - physicalResourceId: { - id: 'LogGroupResourcePolicy', - }, - }, - Delete: { - service: 'CloudWatchLogs', - action: 'deleteResourcePolicy', - parameters: { - policyName: 'TestPolicy', - }, - ignoreErrorCodesMatching: '400', - }, + Create: '{"service":"CloudWatchLogs","action":"putResourcePolicy","parameters":{"policyName":"TestPolicy","policyDocument":"{\\"Statement\\":[{\\"Action\\":[\\"logs:PutLogEvents\\",\\"logs:CreateLogStream\\"],\\"Effect\\":\\"Allow\\",\\"Principal\\":{\\"Service\\":\\"es.amazonaws.com\\"},\\"Resource\\":\\"*\\"}],\\"Version\\":\\"2012-10-17\\"}"},"physicalResourceId":{"id":"LogGroupResourcePolicy"}}', + Update: '{"service":"CloudWatchLogs","action":"putResourcePolicy","parameters":{"policyName":"TestPolicy","policyDocument":"{\\"Statement\\":[{\\"Action\\":[\\"logs:PutLogEvents\\",\\"logs:CreateLogStream\\"],\\"Effect\\":\\"Allow\\",\\"Principal\\":{\\"Service\\":\\"es.amazonaws.com\\"},\\"Resource\\":\\"*\\"}],\\"Version\\":\\"2012-10-17\\"}"},"physicalResourceId":{"id":"LogGroupResourcePolicy"}}', + Delete: '{"service":"CloudWatchLogs","action":"deleteResourcePolicy","parameters":{"policyName":"TestPolicy"},"ignoreErrorCodesMatching":"400"}', }); }); diff --git a/packages/@aws-cdk/aws-events/README.md b/packages/@aws-cdk/aws-events/README.md index d19309455559d..565b0537d091d 100644 --- a/packages/@aws-cdk/aws-events/README.md +++ b/packages/@aws-cdk/aws-events/README.md @@ -163,3 +163,26 @@ In this situation, the CDK will wire the 2 accounts together: For more information, see the [AWS documentation on cross-account events](https://docs.aws.amazon.com/eventbridge/latest/userguide/eventbridge-cross-account-event-delivery.html). + +## Archiving + +It is possible to archive all or some events sent to an event bus. It is then possible to [replay these events](https://aws.amazon.com/blogs/aws/new-archive-and-replay-events-with-amazon-eventbridge/). + +```ts +import * as cdk from '@aws-cdk/core'; + +const stack = new stack(); + +const bus = new EventBus(stack, 'bus', { + eventBusName: 'MyCustomEventBus' +}); + +bus.archive('MyArchive', { + archiveName: 'MyCustomEventBusArchive', + description: 'MyCustomerEventBus Archive', + eventPattern: { + account: [stack.account], + }, + retention: cdk.Duration.days(365), +}); +``` diff --git a/packages/@aws-cdk/aws-events/lib/archive.ts b/packages/@aws-cdk/aws-events/lib/archive.ts new file mode 100644 index 0000000000000..3da79df6682a2 --- /dev/null +++ b/packages/@aws-cdk/aws-events/lib/archive.ts @@ -0,0 +1,77 @@ +import { Duration, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IEventBus } from './event-bus'; +import { EventPattern } from './event-pattern'; +import { CfnArchive } from './events.generated'; + +/** + * The event archive base properties + */ +export interface BaseArchiveProps { + /** + * The name of the archive. + * + * @default - Automatically generated + */ + readonly archiveName?: string; + /** + * A description for the archive. + * + * @default - none + */ + readonly description?: string; + /** + * An event pattern to use to filter events sent to the archive. + */ + readonly eventPattern: EventPattern; + /** + * The number of days to retain events for. Default value is 0. If set to 0, events are retained indefinitely. + * @default - Infinite + */ + readonly retention?: Duration; +} + + +/** + * The event archive properties + */ +export interface ArchiveProps extends BaseArchiveProps { + /** + * The event source associated with the archive. + */ + readonly sourceEventBus: IEventBus; +} + +/** + * Define an EventBridge Archive + * + * @resource AWS::Events::Archive + */ +export class Archive extends Resource { + /** + * The archive name. + * @attribute + */ + public readonly archiveName: string; + + /** + * The ARN of the archive created. + * @attribute + */ + public readonly archiveArn: string; + + constructor(scope: Construct, id: string, props: ArchiveProps) { + super(scope, id, { physicalName: props.archiveName }); + + let archive = new CfnArchive(this, 'Archive', { + sourceArn: props.sourceEventBus.eventBusArn, + description: props.description, + eventPattern: props.eventPattern, + retentionDays: props.retention?.toDays({ integral: true }) || 0, + archiveName: this.physicalName, + }); + + this.archiveArn = archive.attrArn; + this.archiveName = archive.attrArchiveName; + } +} diff --git a/packages/@aws-cdk/aws-events/lib/event-bus.ts b/packages/@aws-cdk/aws-events/lib/event-bus.ts index 27c79c9c7fe3a..cd0c7f913cbf6 100644 --- a/packages/@aws-cdk/aws-events/lib/event-bus.ts +++ b/packages/@aws-cdk/aws-events/lib/event-bus.ts @@ -1,6 +1,7 @@ import * as iam from '@aws-cdk/aws-iam'; import { IResource, Lazy, Names, Resource, Stack, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; +import { Archive, BaseArchiveProps } from './archive'; import { CfnEventBus } from './events.generated'; /** @@ -37,6 +38,15 @@ export interface IEventBus extends IResource { * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-eventbus.html#cfn-events-eventbus-eventsourcename */ readonly eventSourceName?: string; + + /** + * Create an EventBridge archive to send events to. + * When you create an archive, incoming events might not immediately start being sent to the archive. + * Allow a short period of time for changes to take effect. + * + * @param props Properties of the archive + */ + archive(id: string, props: BaseArchiveProps): Archive; } /** @@ -96,12 +106,45 @@ export interface EventBusAttributes { readonly eventSourceName?: string; } +abstract class EventBusBase extends Resource implements IEventBus { + /** + * The physical ID of this event bus resource + */ + public abstract readonly eventBusName: string; + + /** + * The ARN of the event bus, such as: + * arn:aws:events:us-east-2:123456789012:event-bus/aws.partner/PartnerName/acct1/repo1. + */ + public abstract readonly eventBusArn: string; + + /** + * The policy for the event bus in JSON form. + */ + public abstract readonly eventBusPolicy: string; + + /** + * The name of the partner event source + */ + public abstract readonly eventSourceName?: string; + + public archive(id: string, props: BaseArchiveProps): Archive { + return new Archive(this, id, { + sourceEventBus: this, + description: props.description || `Event Archive for ${this.eventBusName} Event Bus`, + eventPattern: props.eventPattern, + retention: props.retention, + archiveName: props.archiveName, + }); + } +} + /** * Define an EventBridge EventBus * * @resource AWS::Events::EventBus */ -export class EventBus extends Resource implements IEventBus { +export class EventBus extends EventBusBase { /** * Import an existing event bus resource @@ -112,13 +155,11 @@ export class EventBus extends Resource implements IEventBus { public static fromEventBusArn(scope: Construct, id: string, eventBusArn: string): IEventBus { const parts = Stack.of(scope).parseArn(eventBusArn); - class Import extends Resource implements IEventBus { - public readonly eventBusArn = eventBusArn; - public readonly eventBusName = parts.resourceName || ''; - public readonly eventBusPolicy = ''; - } - - return new Import(scope, id); + return new ImportedEventBus(scope, id, { + eventBusArn: eventBusArn, + eventBusName: parts.resourceName || '', + eventBusPolicy: '', + }); } /** @@ -128,14 +169,7 @@ export class EventBus extends Resource implements IEventBus { * @param attrs Imported event bus properties */ public static fromEventBusAttributes(scope: Construct, id: string, attrs: EventBusAttributes): IEventBus { - class Import extends Resource implements IEventBus { - public readonly eventBusArn = attrs.eventBusArn; - public readonly eventBusName = attrs.eventBusName; - public readonly eventBusPolicy = attrs.eventBusPolicy; - public readonly eventSourceName = attrs.eventSourceName; - } - - return new Import(scope, id); + return new ImportedEventBus(scope, id, attrs); } /** @@ -241,3 +275,18 @@ export class EventBus extends Resource implements IEventBus { this.eventSourceName = eventBus.eventSourceName; } } + +class ImportedEventBus extends EventBusBase { + public readonly eventBusArn: string; + public readonly eventBusName: string; + public readonly eventBusPolicy: string; + public readonly eventSourceName?: string; + constructor(scope: Construct, id: string, attrs: EventBusAttributes) { + super(scope, id); + + this.eventBusArn = attrs.eventBusArn; + this.eventBusName = attrs.eventBusName; + this.eventBusPolicy = attrs.eventBusPolicy; + this.eventSourceName = attrs.eventSourceName; + } +} diff --git a/packages/@aws-cdk/aws-events/lib/index.ts b/packages/@aws-cdk/aws-events/lib/index.ts index e0aa655afaf72..718b236bf6e91 100644 --- a/packages/@aws-cdk/aws-events/lib/index.ts +++ b/packages/@aws-cdk/aws-events/lib/index.ts @@ -6,6 +6,7 @@ export * from './event-bus'; export * from './event-pattern'; export * from './schedule'; export * from './on-event-options'; +export * from './archive'; // AWS::Events CloudFormation Resources: export * from './events.generated'; diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index 969965021f75d..2c71054b60828 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -295,6 +295,7 @@ export class Rule extends Resource implements IRule { kinesisParameters: targetProps.kinesisParameters, runCommandParameters: targetProps.runCommandParameters, batchParameters: targetProps.batchParameters, + deadLetterConfig: targetProps.deadLetterConfig, sqsParameters: targetProps.sqsParameters, input: inputProps && inputProps.input, inputPath: inputProps && inputProps.inputPath, diff --git a/packages/@aws-cdk/aws-events/lib/target.ts b/packages/@aws-cdk/aws-events/lib/target.ts index 319cf3d4f14da..da2ac9a1c7fb1 100644 --- a/packages/@aws-cdk/aws-events/lib/target.ts +++ b/packages/@aws-cdk/aws-events/lib/target.ts @@ -47,6 +47,12 @@ export interface RuleTargetConfig { */ readonly batchParameters?: CfnRule.BatchParametersProperty; + /** + * Contains information about a dead-letter queue configuration. + * @default no dead-letter queue set + */ + readonly deadLetterConfig?: CfnRule.DeadLetterConfigProperty; + /** * The Amazon ECS task definition and task count to use, if the event target * is an Amazon ECS task. diff --git a/packages/@aws-cdk/aws-events/test/test.archive.ts b/packages/@aws-cdk/aws-events/test/test.archive.ts new file mode 100644 index 0000000000000..fc8f38a516f2b --- /dev/null +++ b/packages/@aws-cdk/aws-events/test/test.archive.ts @@ -0,0 +1,45 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Duration, Stack } from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import { EventBus } from '../lib'; +import { Archive } from '../lib/archive'; + +export = { + 'creates an archive for an EventBus'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + let eventBus = new EventBus(stack, 'Bus'); + + new Archive(stack, 'Archive', { + sourceEventBus: eventBus, + eventPattern: { + account: [stack.account], + }, + retention: Duration.days(10), + }); + + // THEN + expect(stack).to(haveResource('AWS::Events::EventBus', { + Name: 'Bus', + })); + + expect(stack).to(haveResource('AWS::Events::Archive', { + EventPattern: { + account: [{ + Ref: 'AWS::AccountId', + }], + }, + RetentionDays: 10, + SourceArn: { + 'Fn::GetAtt': [ + 'BusEA82B648', + 'Arn', + ], + }, + })); + + test.done(); + }, +} 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 a6c32885ec4a9..2e8434e147bb1 100644 --- a/packages/@aws-cdk/aws-events/test/test.event-bus.ts +++ b/packages/@aws-cdk/aws-events/test/test.event-bus.ts @@ -245,6 +245,133 @@ export = { ], })); + test.done(); + }, + 'can archive events'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const event = new EventBus(stack, 'Bus'); + + event.archive('MyArchive', { + eventPattern: { + account: [stack.account], + }, + archiveName: 'MyArchive', + }); + + // THEN + expect(stack).to(haveResource('AWS::Events::EventBus', { + Name: 'Bus', + })); + + expect(stack).to(haveResource('AWS::Events::Archive', { + SourceArn: { + 'Fn::GetAtt': [ + 'BusEA82B648', + 'Arn', + ], + }, + Description: { + 'Fn::Join': [ + '', + [ + 'Event Archive for ', + { + Ref: 'BusEA82B648', + }, + ' Event Bus', + ], + ], + }, + EventPattern: { + account: [ + { + Ref: 'AWS::AccountId', + }, + ], + }, + RetentionDays: 0, + ArchiveName: 'MyArchive', + })); + + test.done(); + }, + 'can archive events from an imported EventBus'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const bus = new EventBus(stack, 'Bus'); + + const importedBus = EventBus.fromEventBusArn(stack, 'ImportedBus', bus.eventBusArn); + + importedBus.archive('MyArchive', { + eventPattern: { + account: [stack.account], + }, + archiveName: 'MyArchive', + }); + + // THEN + expect(stack).to(haveResource('AWS::Events::EventBus', { + Name: 'Bus', + })); + + expect(stack).to(haveResource('AWS::Events::Archive', { + SourceArn: { + 'Fn::GetAtt': [ + 'BusEA82B648', + 'Arn', + ], + }, + Description: { + 'Fn::Join': [ + '', + [ + 'Event Archive for ', + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '/', + { + 'Fn::Select': [ + 5, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'BusEA82B648', + 'Arn', + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ' Event Bus', + ], + ], + }, + EventPattern: { + account: [ + { + Ref: 'AWS::AccountId', + }, + ], + }, + RetentionDays: 0, + ArchiveName: 'MyArchive', + })); + test.done(); }, }; diff --git a/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts b/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts index c7eb0d87f8fa8..20881d152f396 100644 --- a/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts +++ b/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator-security-group.test.ts @@ -31,29 +31,16 @@ test('custom resource exists', () => { ], }, Create: { - action: 'describeSecurityGroups', - service: 'EC2', - parameters: { - Filters: [ + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"describeSecurityGroups","parameters":{"Filters":[{"Name":"group-name","Values":["GlobalAccelerator"]},{"Name":"vpc-id","Values":["', { - Name: 'group-name', - Values: [ - 'GlobalAccelerator', - ], - }, - { - Name: 'vpc-id', - Values: [ - { - Ref: 'VPCB9E5F0B4', - }, - ], + Ref: 'VPCB9E5F0B4', }, + '"]}]},"physicalResourceId":{"responsePath":"SecurityGroups.0.GroupId"}}', ], - }, - physicalResourceId: { - responsePath: 'SecurityGroups.0.GroupId', - }, + ], }, InstallLatestAwsSdk: true, }, diff --git a/packages/@aws-cdk/aws-glue/package.json b/packages/@aws-cdk/aws-glue/package.json index 0eaa0ba607b63..3f6a8fa63ceba 100644 --- a/packages/@aws-cdk/aws-glue/package.json +++ b/packages/@aws-cdk/aws-glue/package.json @@ -72,6 +72,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-glue/test/table.test.ts b/packages/@aws-cdk/aws-glue/test/table.test.ts index 0969088346491..e31c879208aa2 100644 --- a/packages/@aws-cdk/aws-glue/test/table.test.ts +++ b/packages/@aws-cdk/aws-glue/test/table.test.ts @@ -5,8 +5,12 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as glue from '../lib'; +const s3GrantWriteCtx = { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }; + test('unpartitioned JSON table', () => { const app = new cdk.App(); const dbStack = new cdk.Stack(app, 'db'); @@ -1048,8 +1052,8 @@ test('grants: read only', () => { }); -test('grants: write only', () => { - const stack = new cdk.Stack(); +testFutureBehavior('grants: write only', s3GrantWriteCtx, cdk.App, (app) => { + const stack = new cdk.Stack(app); const user = new iam.User(stack, 'User'); const database = new glue.Database(stack, 'Database', { databaseName: 'database', @@ -1111,7 +1115,7 @@ test('grants: write only', () => { { Action: [ 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], Effect: 'Allow', @@ -1151,8 +1155,8 @@ test('grants: write only', () => { }); -test('grants: read and write', () => { - const stack = new cdk.Stack(); +testFutureBehavior('grants: read and write', s3GrantWriteCtx, cdk.App, (app) => { + const stack = new cdk.Stack(app); const user = new iam.User(stack, 'User'); const database = new glue.Database(stack, 'Database', { databaseName: 'database', @@ -1225,7 +1229,7 @@ test('grants: read and write', () => { 's3:GetBucket*', 's3:List*', 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], Effect: 'Allow', diff --git a/packages/@aws-cdk/aws-iam/lib/oidc-provider.ts b/packages/@aws-cdk/aws-iam/lib/oidc-provider.ts index 91c221ec55652..ec70c6d152cbe 100644 --- a/packages/@aws-cdk/aws-iam/lib/oidc-provider.ts +++ b/packages/@aws-cdk/aws-iam/lib/oidc-provider.ts @@ -155,7 +155,7 @@ export class OpenIdConnectProvider extends Resource implements IOpenIdConnectPro private getOrCreateProvider() { return CustomResourceProvider.getOrCreate(this, RESOURCE_TYPE, { codeDirectory: path.join(__dirname, 'oidc-provider'), - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, policyStatements: [ { Effect: 'Allow', diff --git a/packages/@aws-cdk/aws-kinesis/package.json b/packages/@aws-cdk/aws-kinesis/package.json index e8153880812db..3c7fd281d363b 100644 --- a/packages/@aws-cdk/aws-kinesis/package.json +++ b/packages/@aws-cdk/aws-kinesis/package.json @@ -72,6 +72,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-kinesis/test/stream.test.ts b/packages/@aws-cdk/aws-kinesis/test/stream.test.ts index 9c3cdc1eb7125..b295ac73c75b5 100644 --- a/packages/@aws-cdk/aws-kinesis/test/stream.test.ts +++ b/packages/@aws-cdk/aws-kinesis/test/stream.test.ts @@ -3,6 +3,7 @@ import { arrayWith } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import { App, Duration, Stack, CfnParameter } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior, testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import { Stream, StreamEncryption } from '../lib'; @@ -1255,7 +1256,7 @@ describe('Kinesis data streams', () => { }).toThrow(/'stack.' depends on 'stack.'/); }); - testFutureBehavior('cross stack permissions - with encryption', { '@aws-cdk/aws-kms:defaultKeyPolicies': true }, App, (app) => { + testFutureBehavior('cross stack permissions - with encryption', { [cxapi.KMS_DEFAULT_KEY_POLICIES]: true }, App, (app) => { const stackA = new Stack(app, 'stackA'); const streamFromStackA = new Stream(stackA, 'MyStream', { encryption: StreamEncryption.KMS, diff --git a/packages/@aws-cdk/aws-kms/test/key.test.ts b/packages/@aws-cdk/aws-kms/test/key.test.ts index b2d37496d00ec..38ae0e1e61059 100644 --- a/packages/@aws-cdk/aws-kms/test/key.test.ts +++ b/packages/@aws-cdk/aws-kms/test/key.test.ts @@ -2,6 +2,7 @@ import { arrayWith, ResourcePart } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { testFutureBehavior, testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as kms from '../lib'; @@ -40,7 +41,7 @@ const LEGACY_ADMIN_ACTIONS: string[] = [ 'kms:UntagResource', ]; -const flags = { '@aws-cdk/aws-kms:defaultKeyPolicies': true }; +const flags = { [cxapi.KMS_DEFAULT_KEY_POLICIES]: true }; testFutureBehavior('default key', flags, cdk.App, (app) => { const stack = new cdk.Stack(app); diff --git a/packages/@aws-cdk/aws-lambda-destinations/README.md b/packages/@aws-cdk/aws-lambda-destinations/README.md index 8459820e9686e..404b0b3157adb 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/README.md +++ b/packages/@aws-cdk/aws-lambda-destinations/README.md @@ -21,7 +21,7 @@ The following destinations are supported * SNS topic * EventBridge event bus -Example with a SNS topic for sucessful invocations: +Example with a SNS topic for successful invocations: ```ts import * as lambda from '@aws-cdk/aws-lambda'; 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 f8f6f78713d64..5fc64df8417f3 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 @@ -58,13 +58,13 @@ "Code": { "ZipFile": "exports.handler = async (event) => {\n console.log('Event: %j', event);\n if (event === 'error') throw new Error('UnkownError');\n return event;\n };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "FirstServiceRole097DB3A5", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -114,7 +114,7 @@ ] } }, - "FirstEventInvokeConfigFailureAllowEventRuleawscdklambdachainFirstEventInvokeConfigFailure7180F42FA8F1F1F0": { + "FirstEventInvokeConfigFailureAllowEventRuleawscdklambdachainErrorC073CD8DCAD68018": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -175,7 +175,7 @@ ] } }, - "FirstEventInvokeConfigSuccessAllowEventRuleawscdklambdachainFirstEventInvokeConfigSuccess2DCAE39FC2495AB7": { + "FirstEventInvokeConfigSuccessAllowEventRuleawscdklambdachainSecond178F48F8A8DE2790": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -308,13 +308,13 @@ "Code": { "ZipFile": "exports.handler = async (event) => {\n console.log('Event: %j', event);\n if (event === 'error') throw new Error('UnkownError');\n return event;\n };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "SecondServiceRole55940A31", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -364,7 +364,7 @@ ] } }, - "SecondEventInvokeConfigSuccessAllowEventRuleawscdklambdachainSecondEventInvokeConfigSuccess2078CDC9C7FB9F61": { + "SecondEventInvokeConfigSuccessAllowEventRuleawscdklambdachainThird031C7FF6ABA1C15A": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -453,13 +453,13 @@ "Code": { "ZipFile": "exports.handler = async (event) => {\n console.log('Event: %j', event);\n if (event === 'error') throw new Error('UnkownError');\n return event;\n };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "ThirdServiceRole42701801", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -503,13 +503,13 @@ "Code": { "ZipFile": "exports.handler = async (event) => {\n console.log('Event: %j', event);\n if (event === 'error') throw new Error('UnkownError');\n return event;\n };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "ErrorServiceRoleCE484966", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index f35062120fb9d..d11602c5e656e 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -148,7 +148,7 @@ new lambda.NodejsFunction(this, 'my-handler', { }, logLevel: LogLevel.SILENT, // defaults to LogLevel.WARNING keepNames: true, // defaults to false - tsconfig: 'custom-tsconfig.json' // use custom-tsconfig.json instead of default, + tsconfig: 'custom-tsconfig.json' // use custom-tsconfig.json instead of default, metafile: true, // include meta file, defaults to false banner : '/* comments */', // by default no comments are passed footer : '/* comments */', // by default no comments are passed @@ -220,7 +220,7 @@ Use `bundling.dockerImage` to use a custom Docker bundling image: ```ts new lambda.NodejsFunction(this, 'my-handler', { bundling: { - dockerImage: cdk.BundlingDockerImage.fromAsset('/path/to/Dockerfile'), + dockerImage: cdk.DockerImage.fromBuild('/path/to/Dockerfile'), }, }); ``` diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts index a6dcddde7709d..536ca1ea7646a 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/bundling.ts @@ -140,10 +140,10 @@ export class Bundling implements cdk.BundlingOptions { const esbuildCommand: string = [ npx, 'esbuild', - '--bundle', pathJoin(inputDir, this.relativeEntryPath), + '--bundle', `"${pathJoin(inputDir, this.relativeEntryPath)}"`, `--target=${this.props.target ?? toTarget(this.props.runtime)}`, '--platform=node', - `--outfile=${pathJoin(outputDir, 'index.js')}`, + `--outfile="${pathJoin(outputDir, 'index.js')}"`, ...this.props.minify ? ['--minify'] : [], ...this.props.sourceMap ? ['--sourcemap'] : [], ...this.externals.map(external => `--external:${external}`), diff --git a/packages/@aws-cdk/aws-lambda-nodejs/package.json b/packages/@aws-cdk/aws-lambda-nodejs/package.json index c2e2f658febac..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.50", + "esbuild": "^0.8.55", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts index bd69394ae757c..e6c32c496b2ff 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/bundling.test.ts @@ -20,7 +20,7 @@ beforeEach(() => { getEsBuildVersionMock.mockReturnValue('0.8.8'); fromAssetMock.mockReturnValue({ image: 'built-image', - cp: () => {}, + cp: () => 'dest-path', run: () => {}, toJSON: () => 'built-image', }); @@ -53,7 +53,7 @@ test('esbuild bundling in Docker', () => { }, command: [ 'bash', '-c', - 'npx esbuild --bundle /asset-input/lib/handler.ts --target=node12 --platform=node --outfile=/asset-output/index.js --external:aws-sdk --loader:.png=dataurl', + 'npx esbuild --bundle "/asset-input/lib/handler.ts" --target=node12 --platform=node --outfile="/asset-output/index.js" --external:aws-sdk --loader:.png=dataurl', ], workingDirectory: '/', }), @@ -74,7 +74,7 @@ test('esbuild bundling with handler named index.ts', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'npx esbuild --bundle /asset-input/lib/index.ts --target=node12 --platform=node --outfile=/asset-output/index.js --external:aws-sdk', + 'npx esbuild --bundle "/asset-input/lib/index.ts" --target=node12 --platform=node --outfile="/asset-output/index.js" --external:aws-sdk', ], }), }); @@ -94,7 +94,7 @@ test('esbuild bundling with tsx handler', () => { bundling: expect.objectContaining({ command: [ 'bash', '-c', - 'npx esbuild --bundle /asset-input/lib/handler.tsx --target=node12 --platform=node --outfile=/asset-output/index.js --external:aws-sdk', + 'npx esbuild --bundle "/asset-input/lib/handler.tsx" --target=node12 --platform=node --outfile="/asset-output/index.js" --external:aws-sdk', ], }), }); @@ -139,7 +139,7 @@ test('esbuild bundling with externals and dependencies', () => { command: [ 'bash', '-c', [ - 'npx esbuild --bundle /asset-input/test/bundling.test.js --target=node12 --platform=node --outfile=/asset-output/index.js --external:abc --external:delay', + 'npx esbuild --bundle "/asset-input/test/bundling.test.js" --target=node12 --platform=node --outfile="/asset-output/index.js" --external:abc --external:delay', `echo \'{\"dependencies\":{\"delay\":\"${delayVersion}\"}}\' > /asset-output/package.json`, 'cp /asset-input/package-lock.json /asset-output/package-lock.json', 'cd /asset-output', @@ -181,8 +181,8 @@ test('esbuild bundling with esbuild options', () => { command: [ 'bash', '-c', [ - 'npx esbuild --bundle /asset-input/lib/handler.ts', - '--target=es2020 --platform=node --outfile=/asset-output/index.js', + 'npx esbuild --bundle "/asset-input/lib/handler.ts"', + '--target=es2020 --platform=node --outfile="/asset-output/index.js"', '--minify --sourcemap --external:aws-sdk --loader:.png=dataurl', '--define:DEBUG=true --define:process.env.KEY="VALUE"', '--log-level=silent --keep-names --tsconfig=/asset-input/lib/custom-tsconfig.ts', diff --git a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts index d5747e169abb2..a4446611d9a9d 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/bundling.ts @@ -31,15 +31,56 @@ export interface BundlingOptions { * Output path suffix ('python' for a layer, '.' otherwise) */ readonly outputPathSuffix: string; + + /** + * Determines how asset hash is calculated. Assets will get rebuild and + * uploaded only if their hash has changed. + * + * If asset hash is set to `SOURCE` (default), then only changes to the source + * directory will cause the asset to rebuild. This means, for example, that in + * order to pick up a new dependency version, a change must be made to the + * source tree. Ideally, this can be implemented by including a dependency + * lockfile in your source tree or using fixed dependencies. + * + * If the asset hash is set to `OUTPUT`, the hash is calculated after + * bundling. This means that any change in the output will cause the asset to + * be invalidated and uploaded. Bear in mind that `pip` adds timestamps to + * dependencies it installs, which implies that in this mode Python bundles + * will _always_ get rebuild and uploaded. Normally this is an anti-pattern + * since build + * + * @default AssetHashType.SOURCE By default, hash is calculated based on the + * contents of the source directory. If `assetHash` is also specified, the + * default is `CUSTOM`. This means that only updates to the source will cause + * the asset to rebuild. + */ + readonly assetHashType?: cdk.AssetHashType; + + /** + * Specify a custom hash for this asset. If `assetHashType` is set it must + * be set to `AssetHashType.CUSTOM`. For consistency, this custom hash will + * be SHA256 hashed and encoded as hex. The resulting hash will be the asset + * hash. + * + * NOTE: the hash is used in order to identify a specific revision of the asset, and + * used for optimizing and caching deployment activities related to this asset such as + * packaging, uploading to Amazon S3, etc. If you chose to customize the hash, you will + * need to make sure it is updated every time the asset changes, or otherwise it is + * possible that some deployments will not be invalidated. + * + * @default - based on `assetHashType` + */ + readonly assetHash?: string; } /** * Produce bundled Lambda asset code */ -export function bundle(options: BundlingOptions): lambda.AssetCode { +export function bundle(options: BundlingOptions): lambda.Code { const { entry, runtime, outputPathSuffix } = options; - const hasDeps = hasDependencies(entry); + const stagedir = cdk.FileSystem.mkdtemp('python-bundling-'); + const hasDeps = stageDependencies(entry, stagedir); const depsCommand = chain([ hasDeps ? `rsync -r ${BUNDLER_DEPENDENCIES_CACHE}/. ${cdk.AssetStaging.BUNDLING_OUTPUT_DIR}/${outputPathSuffix}` : '', @@ -54,15 +95,19 @@ export function bundle(options: BundlingOptions): lambda.AssetCode { ? 'Dockerfile.dependencies' : 'Dockerfile'; - const image = cdk.BundlingDockerImage.fromAsset(entry, { + // copy Dockerfile to workdir + fs.copyFileSync(path.join(__dirname, dockerfile), path.join(stagedir, dockerfile)); + + const image = cdk.BundlingDockerImage.fromAsset(stagedir, { buildArgs: { IMAGE: runtime.bundlingDockerImage.image, }, - file: path.join(__dirname, dockerfile), + file: dockerfile, }); return lambda.Code.fromAsset(entry, { - assetHashType: cdk.AssetHashType.BUNDLE, + assetHashType: options.assetHashType, + assetHash: options.assetHash, exclude: DEPENDENCY_EXCLUDES, bundling: { image, @@ -75,20 +120,25 @@ export function bundle(options: BundlingOptions): lambda.AssetCode { * Checks to see if the `entry` directory contains a type of dependency that * we know how to install. */ -export function hasDependencies(entry: string): boolean { - if (fs.existsSync(path.join(entry, 'Pipfile'))) { - return true; - } - - if (fs.existsSync(path.join(entry, 'poetry.lock'))) { - return true; - } - - if (fs.existsSync(path.join(entry, 'requirements.txt'))) { - return true; +export function stageDependencies(entry: string, stagedir: string): boolean { + const prefixes = [ + 'Pipfile', + 'pyproject', + 'poetry', + 'requirements.txt', + ]; + + let found = false; + for (const file of fs.readdirSync(entry)) { + for (const prefix of prefixes) { + if (file.startsWith(prefix)) { + fs.copyFileSync(path.join(entry, file), path.join(stagedir, file)); + found = true; + } + } } - return false; + return found; } function chain(commands: string[]): string { diff --git a/packages/@aws-cdk/aws-lambda-python/lib/function.ts b/packages/@aws-cdk/aws-lambda-python/lib/function.ts index 267245738cbe6..733c115c0383d 100644 --- a/packages/@aws-cdk/aws-lambda-python/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda-python/lib/function.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as lambda from '@aws-cdk/aws-lambda'; +import { AssetHashType } from '@aws-cdk/core'; import { bundle } from './bundling'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main @@ -37,6 +38,45 @@ export interface PythonFunctionProps extends lambda.FunctionOptions { * @default lambda.Runtime.PYTHON_3_7 */ readonly runtime?: lambda.Runtime; + + /** + * Determines how asset hash is calculated. Assets will get rebuild and + * uploaded only if their hash has changed. + * + * If asset hash is set to `SOURCE` (default), then only changes to the source + * directory will cause the asset to rebuild. This means, for example, that in + * order to pick up a new dependency version, a change must be made to the + * source tree. Ideally, this can be implemented by including a dependency + * lockfile in your source tree or using fixed dependencies. + * + * If the asset hash is set to `OUTPUT`, the hash is calculated after + * bundling. This means that any change in the output will cause the asset to + * be invalidated and uploaded. Bear in mind that `pip` adds timestamps to + * dependencies it installs, which implies that in this mode Python bundles + * will _always_ get rebuild and uploaded. Normally this is an anti-pattern + * since build + * + * @default AssetHashType.SOURCE By default, hash is calculated based on the + * contents of the source directory. This means that only updates to the + * source will cause the asset to rebuild. + */ + readonly assetHashType?: AssetHashType; + + /** + * Specify a custom hash for this asset. If `assetHashType` is set it must + * be set to `AssetHashType.CUSTOM`. For consistency, this custom hash will + * be SHA256 hashed and encoded as hex. The resulting hash will be the asset + * hash. + * + * NOTE: the hash is used in order to identify a specific revision of the asset, and + * used for optimizing and caching deployment activities related to this asset such as + * packaging, uploading to Amazon S3, etc. If you chose to customize the hash, you will + * need to make sure it is updated every time the asset changes, or otherwise it is + * possible that some deployments will not be invalidated. + * + * @default - based on `assetHashType` + */ + readonly assetHash?: string; } /** @@ -70,6 +110,8 @@ export class PythonFunction extends lambda.Function { runtime, entry, outputPathSuffix: '.', + assetHashType: props.assetHashType, + assetHash: props.assetHash, }), handler: `${index.slice(0, -3)}.${handler}`, }); diff --git a/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts index 4286d092adf8f..d6de4ee23aedd 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/bundling.test.ts @@ -1,11 +1,10 @@ import * as fs from 'fs'; import * as path from 'path'; import { Code, Runtime } from '@aws-cdk/aws-lambda'; -import { hasDependencies, bundle } from '../lib/bundling'; +import { FileSystem } from '@aws-cdk/core'; +import { stageDependencies, bundle } from '../lib/bundling'; jest.mock('@aws-cdk/aws-lambda'); -const existsSyncOriginal = fs.existsSync; -const existsSyncMock = jest.spyOn(fs, 'existsSync'); jest.mock('child_process', () => ({ spawnSync: jest.fn(() => { @@ -41,9 +40,6 @@ test('Bundling a function without dependencies', () => { ], }), })); - - // Searches for requirements.txt in entry - expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt')); }); test('Bundling a function with requirements.txt installed', () => { @@ -63,9 +59,6 @@ test('Bundling a function with requirements.txt installed', () => { ], }), })); - - // Searches for requirements.txt in entry - expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt')); }); test('Bundling Python 2.7 with requirements.txt installed', () => { @@ -85,9 +78,6 @@ test('Bundling Python 2.7 with requirements.txt installed', () => { ], }), })); - - // Searches for requirements.txt in entry - expect(existsSyncMock).toHaveBeenCalledWith(path.join(entry, 'requirements.txt')); }); test('Bundling a layer with dependencies', () => { @@ -128,42 +118,24 @@ test('Bundling a python code layer', () => { })); }); -describe('Dependency detection', () => { - test('Detects pipenv', () => { - existsSyncMock.mockImplementation((p: fs.PathLike) => { - if (/Pipfile/.test(p.toString())) { - return true; - } - return existsSyncOriginal(p); - }); - - expect(hasDependencies('/asset-input')).toEqual(true); - }); - - test('Detects poetry', () => { - existsSyncMock.mockImplementation((p: fs.PathLike) => { - if (/poetry.lock/.test(p.toString())) { - return true; - } - return existsSyncOriginal(p); - }); - - expect(hasDependencies('/asset-input')).toEqual(true); - }); - test('Detects requirements.txt', () => { - existsSyncMock.mockImplementation((p: fs.PathLike) => { - if (/requirements.txt/.test(p.toString())) { - return true; - } - return existsSyncOriginal(p); - }); - - expect(hasDependencies('/asset-input')).toEqual(true); +describe('Dependency detection', () => { + test.each(['Pipfile', 'poetry.lock', 'requirements.txt'])('detect dependency %p', filename => { + // GIVEN + const sourcedir = FileSystem.mkdtemp('source-'); + const stagedir = FileSystem.mkdtemp('stage-'); + fs.writeFileSync(path.join(sourcedir, filename), 'dummy!'); + + // WHEN + const found = stageDependencies(sourcedir, stagedir); + + // THEN + expect(found).toBeTruthy(); + expect(fs.existsSync(path.join(stagedir, filename))).toBeTruthy(); }); test('No known dependencies', () => { - existsSyncMock.mockImplementation(() => false); - expect(hasDependencies('/asset-input')).toEqual(false); + const sourcedir = FileSystem.mkdtemp('source-'); + expect(stageDependencies(sourcedir, '/dummy')).toEqual(false); }); }); diff --git a/packages/@aws-cdk/aws-lambda-python/test/function.test.ts b/packages/@aws-cdk/aws-lambda-python/test/function.test.ts index 98bab1cf35be4..d1f9d64241b61 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/function.test.ts @@ -1,16 +1,36 @@ import '@aws-cdk/assert/jest'; -import { Runtime } from '@aws-cdk/aws-lambda'; -import { Stack } from '@aws-cdk/core'; +import { Code, Runtime } from '@aws-cdk/aws-lambda'; +import { AssetHashType, AssetOptions, Stack } from '@aws-cdk/core'; import { PythonFunction } from '../lib'; import { bundle } from '../lib/bundling'; jest.mock('../lib/bundling', () => { return { - bundle: jest.fn().mockReturnValue({ - bind: () => { - return { inlineCode: 'code' }; - }, - bindToResource: () => { return; }, + bundle: jest.fn().mockImplementation((options: AssetOptions): Code => { + const mockObjectKey = (() => { + const hashType = options.assetHashType ?? (options.assetHash ? 'custom' : 'source'); + switch (hashType) { + case 'source': return 'SOURCE_MOCK'; + case 'output': return 'OUTPUT_MOCK'; + case 'custom': { + if (!options.assetHash) { throw new Error('no custom hash'); } + return options.assetHash; + } + } + + throw new Error('unexpected asset hash type'); + })(); + + return { + isInline: false, + bind: () => ({ + s3Location: { + bucketName: 'mock-bucket-name', + objectKey: mockObjectKey, + }, + }), + bindToResource: () => { return; }, + }; }), hasDependencies: jest.fn().mockReturnValue(false), }; @@ -73,3 +93,53 @@ test('throws with the wrong runtime family', () => { runtime: Runtime.NODEJS_12_X, })).toThrow(/Only `PYTHON` runtimes are supported/); }); + +test('allows specifying hash type', () => { + new PythonFunction(stack, 'source1', { + entry: 'test/lambda-handler-nodeps', + index: 'index.py', + handler: 'handler', + }); + + new PythonFunction(stack, 'source2', { + entry: 'test/lambda-handler-nodeps', + index: 'index.py', + handler: 'handler', + assetHashType: AssetHashType.SOURCE, + }); + + new PythonFunction(stack, 'output', { + entry: 'test/lambda-handler-nodeps', + index: 'index.py', + handler: 'handler', + assetHashType: AssetHashType.OUTPUT, + }); + + new PythonFunction(stack, 'custom', { + entry: 'test/lambda-handler-nodeps', + index: 'index.py', + handler: 'handler', + assetHash: 'MY_CUSTOM_HASH', + }); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Code: { + S3Bucket: 'mock-bucket-name', + S3Key: 'SOURCE_MOCK', + }, + }); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Code: { + S3Bucket: 'mock-bucket-name', + S3Key: 'OUTPUT_MOCK', + }, + }); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + Code: { + S3Bucket: 'mock-bucket-name', + S3Key: 'MY_CUSTOM_HASH', + }, + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json index 3690005685439..45fb46b70aeb9 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3Bucket414E0E30" + "Ref": "AssetParameters79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589acS3Bucket424FEB44" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098" + "Ref": "AssetParameters79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589acS3VersionKeyCEB2635C" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098" + "Ref": "AssetParameters79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589acS3VersionKeyCEB2635C" } ] } @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3Bucket414E0E30": { + "AssetParameters79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589acS3Bucket424FEB44": { "Type": "String", - "Description": "S3 bucket for asset \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" + "Description": "S3 bucket for asset \"79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589ac\"" }, - "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098": { + "AssetParameters79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589acS3VersionKeyCEB2635C": { "Type": "String", - "Description": "S3 key for asset version \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" + "Description": "S3 key for asset version \"79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589ac\"" }, - "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353ArtifactHashECA6C88C": { + "AssetParameters79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589acArtifactHashE38133D4": { "Type": "String", - "Description": "Artifact hash for asset \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" + "Description": "Artifact hash for asset \"79d8f328899b90e2c16929e9393ebf344f098abde8981abdff0168fc9b0589ac\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.expected.json index ef1f355e528c3..aa7bc5fbbcd68 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3BucketA501FC08" + "Ref": "AssetParameters50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0bS3Bucket4125B4E4" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3VersionKey1C3AFB39" + "Ref": "AssetParameters50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0bS3VersionKey1CF28CF5" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3VersionKey1C3AFB39" + "Ref": "AssetParameters50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0bS3VersionKey1CF28CF5" } ] } @@ -121,7 +121,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3Bucket7DE4D4D5" + "Ref": "AssetParameters314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076S3BucketD6F6E4F2" }, "S3Key": { "Fn::Join": [ @@ -134,7 +134,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3VersionKeyAEB67E87" + "Ref": "AssetParameters314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076S3VersionKeyE0B47D8E" } ] } @@ -147,7 +147,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3VersionKeyAEB67E87" + "Ref": "AssetParameters314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076S3VersionKeyE0B47D8E" } ] } @@ -206,7 +206,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3BucketA66E9035" + "Ref": "AssetParameters11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2S3BucketE17A9F3E" }, "S3Key": { "Fn::Join": [ @@ -219,7 +219,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3VersionKeyAFEB5FDA" + "Ref": "AssetParameters11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2S3VersionKey0A0F7F93" } ] } @@ -232,7 +232,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3VersionKeyAFEB5FDA" + "Ref": "AssetParameters11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2S3VersionKey0A0F7F93" } ] } @@ -257,41 +257,41 @@ } }, "Parameters": { - "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3BucketA501FC08": { + "AssetParameters50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0bS3Bucket4125B4E4": { "Type": "String", - "Description": "S3 bucket for asset \"94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45c\"" + "Description": "S3 bucket for asset \"50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0b\"" }, - "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cS3VersionKey1C3AFB39": { + "AssetParameters50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0bS3VersionKey1CF28CF5": { "Type": "String", - "Description": "S3 key for asset version \"94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45c\"" + "Description": "S3 key for asset version \"50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0b\"" }, - "AssetParameters94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45cArtifactHash99DC751A": { + "AssetParameters50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0bArtifactHashC28E4EDF": { "Type": "String", - "Description": "Artifact hash for asset \"94972df8a01484c56b50bec3793ac6c4302bc044db29d3502007bdc0f83db45c\"" + "Description": "Artifact hash for asset \"50fe6f46cf18fb257f00fed007da5c80fbf5dee08bec37fe765c50188eecae0b\"" }, - "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3Bucket7DE4D4D5": { + "AssetParameters314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076S3BucketD6F6E4F2": { "Type": "String", - "Description": "S3 bucket for asset \"3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28\"" + "Description": "S3 bucket for asset \"314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076\"" }, - "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28S3VersionKeyAEB67E87": { + "AssetParameters314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076S3VersionKeyE0B47D8E": { "Type": "String", - "Description": "S3 key for asset version \"3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28\"" + "Description": "S3 key for asset version \"314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076\"" }, - "AssetParameters3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28ArtifactHashE51CE860": { + "AssetParameters314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076ArtifactHash45C2DF56": { "Type": "String", - "Description": "Artifact hash for asset \"3b0b0f3cd46ea1490006d6cefca359385ec059bb00a0fbee4de2eecf48038e28\"" + "Description": "Artifact hash for asset \"314bcf2cee9a5b01d3be2c2815602d1d784611fac220dde672aca6cb54299076\"" }, - "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3BucketA66E9035": { + "AssetParameters11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2S3BucketE17A9F3E": { "Type": "String", - "Description": "S3 bucket for asset \"876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009\"" + "Description": "S3 bucket for asset \"11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2\"" }, - "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009S3VersionKeyAFEB5FDA": { + "AssetParameters11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2S3VersionKey0A0F7F93": { "Type": "String", - "Description": "S3 key for asset version \"876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009\"" + "Description": "S3 key for asset version \"11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2\"" }, - "AssetParameters876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009ArtifactHashB9A1080D": { + "AssetParameters11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2ArtifactHash743A82BD": { "Type": "String", - "Description": "Artifact hash for asset \"876959f777c5a23bf4408991959c55c91810329d159608feb7ede69418b35009\"" + "Description": "Artifact hash for asset \"11846740cde0308720709e44dce627bf1ceb557ee9d0dbb556a05632da565ef2\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.poetry.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.poetry.expected.json index 5ea17bca31920..c2ad372f25649 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.poetry.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.poetry.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3BucketD53ED9C5" + "Ref": "AssetParameters3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4S3Bucket68AF28A9" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3VersionKey3C218A3E" + "Ref": "AssetParameters3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4S3VersionKeyACC0084B" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3VersionKey3C218A3E" + "Ref": "AssetParameters3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4S3VersionKeyACC0084B" } ] } @@ -121,7 +121,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3BucketFDE171D0" + "Ref": "AssetParametersc1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17S3Bucket980A99E7" }, "S3Key": { "Fn::Join": [ @@ -134,7 +134,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3VersionKey6209E240" + "Ref": "AssetParametersc1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17S3VersionKeyC4E1E9B5" } ] } @@ -147,7 +147,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3VersionKey6209E240" + "Ref": "AssetParametersc1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17S3VersionKeyC4E1E9B5" } ] } @@ -206,7 +206,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3BucketA23E6312" + "Ref": "AssetParameters3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6S3Bucket91AABE39" }, "S3Key": { "Fn::Join": [ @@ -219,7 +219,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3VersionKey1E21AF83" + "Ref": "AssetParameters3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6S3VersionKeyEE0FAD90" } ] } @@ -232,7 +232,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3VersionKey1E21AF83" + "Ref": "AssetParameters3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6S3VersionKeyEE0FAD90" } ] } @@ -257,41 +257,41 @@ } }, "Parameters": { - "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3BucketD53ED9C5": { + "AssetParameters3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4S3Bucket68AF28A9": { "Type": "String", - "Description": "S3 bucket for asset \"61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175ef\"" + "Description": "S3 bucket for asset \"3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4\"" }, - "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efS3VersionKey3C218A3E": { + "AssetParameters3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4S3VersionKeyACC0084B": { "Type": "String", - "Description": "S3 key for asset version \"61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175ef\"" + "Description": "S3 key for asset version \"3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4\"" }, - "AssetParameters61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175efArtifactHash6A1881A8": { + "AssetParameters3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4ArtifactHashC2D4B1C3": { "Type": "String", - "Description": "Artifact hash for asset \"61d8d26f10d1d73dee2732bec7ed381d2c987fc2912a339f2f119f3b0ea175ef\"" + "Description": "Artifact hash for asset \"3bc4d0e28b60c2b2468004185dabbe33a91c04563872cafb35b5b71e8a8f33d4\"" }, - "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3BucketFDE171D0": { + "AssetParametersc1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17S3Bucket980A99E7": { "Type": "String", - "Description": "S3 bucket for asset \"1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956d\"" + "Description": "S3 bucket for asset \"c1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17\"" }, - "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dS3VersionKey6209E240": { + "AssetParametersc1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17S3VersionKeyC4E1E9B5": { "Type": "String", - "Description": "S3 key for asset version \"1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956d\"" + "Description": "S3 key for asset version \"c1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17\"" }, - "AssetParameters1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956dArtifactHash02B929EC": { + "AssetParametersc1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17ArtifactHash5B02FA4D": { "Type": "String", - "Description": "Artifact hash for asset \"1d66b06c3b3ee86b3126fb58d7a06ff055d366d8aeeb4dfbaf28d40f0930956d\"" + "Description": "Artifact hash for asset \"c1614067648b8b7e151e321ce82879d259a2b8f2bd10dddd61f0f2ce26287c17\"" }, - "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3BucketA23E6312": { + "AssetParameters3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6S3Bucket91AABE39": { "Type": "String", - "Description": "S3 bucket for asset \"96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68\"" + "Description": "S3 bucket for asset \"3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6\"" }, - "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68S3VersionKey1E21AF83": { + "AssetParameters3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6S3VersionKeyEE0FAD90": { "Type": "String", - "Description": "S3 key for asset version \"96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68\"" + "Description": "S3 key for asset version \"3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6\"" }, - "AssetParameters96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68ArtifactHash0043D2A0": { + "AssetParameters3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6ArtifactHash09CEB444": { "Type": "String", - "Description": "Artifact hash for asset \"96a447e468bf9d3b52d13213757160cd43f28737a29b8682c281fde388762e68\"" + "Description": "Artifact hash for asset \"3610bde00ecd0013f7806e2ab0e80d7ac26232cd3ffc2934b5ca28fef120bdf6\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json index 9a81c901d7451..aa13e73295e64 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.expected.json @@ -5,7 +5,7 @@ "Properties": { "Content": { "S3Bucket": { - "Ref": "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3BucketCCD07444" + "Ref": "AssetParameterse174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66eS3Bucket7A00FCB4" }, "S3Key": { "Fn::Join": [ @@ -18,7 +18,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3VersionKeyA8B74284" + "Ref": "AssetParameterse174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66eS3VersionKey8BF2F9D6" } ] } @@ -31,7 +31,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3VersionKeyA8B74284" + "Ref": "AssetParameterse174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66eS3VersionKey8BF2F9D6" } ] } @@ -82,7 +82,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3Bucket89C9DB12" + "Ref": "AssetParameters907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafbS3Bucket16F02289" }, "S3Key": { "Fn::Join": [ @@ -95,7 +95,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3VersionKey435DAD55" + "Ref": "AssetParameters907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafbS3VersionKeyDAF0A5BD" } ] } @@ -108,7 +108,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3VersionKey435DAD55" + "Ref": "AssetParameters907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafbS3VersionKeyDAF0A5BD" } ] } @@ -138,29 +138,29 @@ } }, "Parameters": { - "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3BucketCCD07444": { + "AssetParameterse174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66eS3Bucket7A00FCB4": { "Type": "String", - "Description": "S3 bucket for asset \"6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4a\"" + "Description": "S3 bucket for asset \"e174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66e\"" }, - "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aS3VersionKeyA8B74284": { + "AssetParameterse174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66eS3VersionKey8BF2F9D6": { "Type": "String", - "Description": "S3 key for asset version \"6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4a\"" + "Description": "S3 key for asset version \"e174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66e\"" }, - "AssetParameters6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4aArtifactHashB3093591": { + "AssetParameterse174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66eArtifactHash2DECF34E": { "Type": "String", - "Description": "Artifact hash for asset \"6a4b9ce26d3228c4effd7b46ed51ab439e79a530934ad9bde7d77d7f6b6ebd4a\"" + "Description": "Artifact hash for asset \"e174a6a88cb48eb510c29b2bf0203c181cfa059320745e6ae6429e522b36c66e\"" }, - "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3Bucket89C9DB12": { + "AssetParameters907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafbS3Bucket16F02289": { "Type": "String", - "Description": "S3 bucket for asset \"71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218\"" + "Description": "S3 bucket for asset \"907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafb\"" }, - "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218S3VersionKey435DAD55": { + "AssetParameters907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafbS3VersionKeyDAF0A5BD": { "Type": "String", - "Description": "S3 key for asset version \"71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218\"" + "Description": "S3 key for asset version \"907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafb\"" }, - "AssetParameters71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218ArtifactHash0EDF3CD0": { + "AssetParameters907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafbArtifactHashD7592B0F": { "Type": "String", - "Description": "Artifact hash for asset \"71de8786d26e9f9205375b6cea9342e92d8a622a97d01d7e7d2f7661f056f218\"" + "Description": "Artifact hash for asset \"907ec4b12820d4b2dc64e7f0f1f1f6267c1db622bc42aa57f5361efa12b1aafb\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json index b5b137205752f..c49f5e312d873 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3BucketA9379638" + "Ref": "AssetParameters6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413dS3Bucket74CBB570" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3VersionKey4376B462" + "Ref": "AssetParameters6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413dS3VersionKey3CBAF21A" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3VersionKey4376B462" + "Ref": "AssetParameters6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413dS3VersionKey3CBAF21A" } ] } @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3BucketA9379638": { + "AssetParameters6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413dS3Bucket74CBB570": { "Type": "String", - "Description": "S3 bucket for asset \"1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148ad\"" + "Description": "S3 bucket for asset \"6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413d\"" }, - "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adS3VersionKey4376B462": { + "AssetParameters6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413dS3VersionKey3CBAF21A": { "Type": "String", - "Description": "S3 key for asset version \"1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148ad\"" + "Description": "S3 key for asset version \"6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413d\"" }, - "AssetParameters1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148adArtifactHashB9B928DC": { + "AssetParameters6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413dArtifactHash750F3AF8": { "Type": "String", - "Description": "Artifact hash for asset \"1482f01217b8bed41000ca172724dc762f68208d3faa315bd6e8e07bbea148ad\"" + "Description": "Artifact hash for asset \"6cc4994756d4085a860e734568c92826773e52c22c58894ce368b1e698da413d\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json index 6b3b8230c2874..c70f4a0ca8933 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersaf41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966aS3BucketEE202B67" + "Ref": "AssetParameters6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280dS3Bucket2F189DB9" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersaf41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966aS3VersionKey8097C675" + "Ref": "AssetParameters6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280dS3VersionKeyDF03C812" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersaf41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966aS3VersionKey8097C675" + "Ref": "AssetParameters6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280dS3VersionKeyDF03C812" } ] } @@ -87,17 +87,17 @@ } }, "Parameters": { - "AssetParametersaf41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966aS3BucketEE202B67": { + "AssetParameters6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280dS3Bucket2F189DB9": { "Type": "String", - "Description": "S3 bucket for asset \"af41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966a\"" + "Description": "S3 bucket for asset \"6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280d\"" }, - "AssetParametersaf41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966aS3VersionKey8097C675": { + "AssetParameters6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280dS3VersionKeyDF03C812": { "Type": "String", - "Description": "S3 key for asset version \"af41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966a\"" + "Description": "S3 key for asset version \"6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280d\"" }, - "AssetParametersaf41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966aArtifactHash3E92B1F8": { + "AssetParameters6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280dArtifactHash1F692755": { "Type": "String", - "Description": "Artifact hash for asset \"af41a5381eff9302e9acdfeb9c3bcf160b56a97091242b2d599ed5a861af966a\"" + "Description": "Artifact hash for asset \"6fa8e0c54d06a6402126a86fab5da1fa1397bcce628a0fb56f8356a2edf6280d\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json b/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json index 63fad4c61de14..6787327a15e40 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.expected.json @@ -296,7 +296,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3Bucket414E0E30" + "Ref": "AssetParameters83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530S3Bucket82392CBF" }, "S3Key": { "Fn::Join": [ @@ -309,7 +309,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098" + "Ref": "AssetParameters83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530S3VersionKey292822E7" } ] } @@ -322,7 +322,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098" + "Ref": "AssetParameters83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530S3VersionKey292822E7" } ] } @@ -368,17 +368,17 @@ } }, "Parameters": { - "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3Bucket414E0E30": { + "AssetParameters83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530S3Bucket82392CBF": { "Type": "String", - "Description": "S3 bucket for asset \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" + "Description": "S3 bucket for asset \"83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530\"" }, - "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353S3VersionKey5ABC9098": { + "AssetParameters83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530S3VersionKey292822E7": { "Type": "String", - "Description": "S3 key for asset version \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" + "Description": "S3 key for asset version \"83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530\"" }, - "AssetParameters4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353ArtifactHashECA6C88C": { + "AssetParameters83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530ArtifactHash8818CE02": { "Type": "String", - "Description": "Artifact hash for asset \"4ee6ce8b6ee4bd51743dc0c39d6e52baebaeafff9c9dfea0ff84de98d1dbf353\"" + "Description": "Artifact hash for asset \"83be407310ab5911f40fa4934091a233f92ce3be1d81c48846f30fa0a9330530\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/layer.test.ts b/packages/@aws-cdk/aws-lambda-python/test/layer.test.ts index 8ace2ec3f7a18..255adb0dae646 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/layer.test.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/layer.test.ts @@ -2,7 +2,7 @@ import '@aws-cdk/assert/jest'; import * as path from 'path'; import { Runtime } from '@aws-cdk/aws-lambda'; import { Stack } from '@aws-cdk/core'; -import { hasDependencies, bundle } from '../lib/bundling'; +import { stageDependencies, bundle } from '../lib/bundling'; import { PythonLayerVersion } from '../lib/layer'; jest.mock('../lib/bundling', () => { @@ -18,11 +18,11 @@ jest.mock('../lib/bundling', () => { }, bindToResource: () => { return; }, }), - hasDependencies: jest.fn().mockReturnValue(true), + stageDependencies: jest.fn().mockReturnValue(true), }; }); -const hasDependenciesMock = (hasDependencies as jest.Mock); +const hasDependenciesMock = (stageDependencies as jest.Mock); let stack: Stack; beforeEach(() => { diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 98994962ec129..ae21d6ea9216e 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -18,7 +18,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as path from 'path'; const fn = new lambda.Function(this, 'MyFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), }); @@ -36,6 +36,9 @@ runtime code. * `lambda.Code.fromAsset(path)` - specify a directory or a .zip file in the local filesystem which will be zipped and uploaded to S3 before deployment. See also [bundling asset code](#bundling-asset-code). + * `lambda.Code.fromDockerBuild(path, options)` - use the result of a Docker + build as code. The runtime code is expected to be located at `/asset` in the + image and will be zipped and uploaded to S3 as an asset. The following example shows how to define a Python function and deploy the code from the local directory `my-lambda-handler` to it: @@ -88,7 +91,7 @@ function. To reference the autogenerated Role: ```ts const fn = new lambda.Function(this, 'MyFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), @@ -101,7 +104,7 @@ it appropriate permissions: ```ts const fn = new lambda.Function(this, 'MyFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), role: myRole // user-provided role @@ -251,7 +254,7 @@ setting the `deadLetterQueueEnabled: true` configuration. import * as lambda from '@aws-cdk/aws-lambda'; const fn = new lambda.Function(this, 'MyFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromInline('exports.handler = function(event, ctx, cb) { return cb(null, "hi"); }'), deadLetterQueueEnabled: true @@ -266,7 +269,7 @@ import * as sqs from '@aws-cdk/aws-sqs'; const dlq = new sqs.Queue(this, 'DLQ'); const fn = new lambda.Function(this, 'MyFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromInline('exports.handler = function(event, ctx, cb) { return cb(null, "hi"); }'), deadLetterQueue: dlq @@ -282,7 +285,7 @@ to learn more about AWS Lambdas and DLQs. import * as lambda from '@aws-cdk/aws-lambda'; const fn = new lambda.Function(this, 'MyFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromInline('exports.handler = function(event, ctx, cb) { return cb(null, "hi"); }'), tracing: lambda.Tracing.ACTIVE @@ -321,7 +324,7 @@ to learn more about AWS Lambda's Profiling support. import * as lambda from '@aws-cdk/aws-lambda'; const fn = new lambda.Function(this, 'MyFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromInline('exports.handler = function(event, ctx, cb) { return cb(null, "hi"); }'), reservedConcurrentExecutions: 100 @@ -450,7 +453,7 @@ new lambda.Function(this, 'Function', { bundling: { image: lambda.Runtime.PYTHON_3_6.bundlingDockerImage, command: [ - 'bash', '-c', + 'bash', '-c', 'pip install -r requirements.txt -t /asset-output && cp -au . /asset-output' ], }, @@ -462,8 +465,8 @@ new lambda.Function(this, 'Function', { Runtimes expose a `bundlingDockerImage` property that points to the [AWS SAM](https://github.com/awslabs/aws-sam-cli) build image. -Use `cdk.BundlingDockerImage.fromRegistry(image)` to use an existing image or -`cdk.BundlingDockerImage.fromAsset(path)` to build a specific image: +Use `cdk.DockerImage.fromRegistry(image)` to use an existing image or +`cdk.DockerImage.fromBuild(path)` to build a specific image: ```ts import * as cdk from '@aws-cdk/core'; @@ -471,7 +474,7 @@ import * as cdk from '@aws-cdk/core'; new lambda.Function(this, 'Function', { code: lambda.Code.fromAsset('/path/to/handler', { bundling: { - image: cdk.BundlingDockerImage.fromAsset('/path/to/dir/with/DockerFile', { + image: cdk.DockerImage.fromBuild('/path/to/dir/with/DockerFile', { buildArgs: { ARG1: 'value1', }, @@ -489,3 +492,27 @@ Language-specific higher level constructs are provided in separate modules: * `@aws-cdk/aws-lambda-nodejs`: [Github](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-lambda-nodejs) & [CDK Docs](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-lambda-nodejs-readme.html) * `@aws-cdk/aws-lambda-python`: [Github](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-lambda-python) & [CDK Docs](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-lambda-python-readme.html) + +## Code Signing + +Code signing for AWS Lambda helps to ensure that only trusted code runs in your Lambda functions. +When enabled, AWS Lambda checks every code deployment and verifies that the code package is signed by a trusted source. +For more information, see [Configuring code signing for AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/configuration-codesigning.html). +The following code configures a function with code signing. + +```typescript +import * as signer from '@aws-cdk/aws-signer'; + +const signerProfile = signer.SigningProfile(this, 'SigningProfile', { + platform: Platform.AWS_LAMBDA_SHA384_ECDSA +}); + +const codeSigningConfig = new lambda.CodeSigningConfig(stack, 'CodeSigningConfig', { + signingProfiles: [signingProfile], +}); + +new lambda.Function(this, 'Function', { + codeSigningConfig, + // ... +}); +``` diff --git a/packages/@aws-cdk/aws-lambda/lib/code-signing-config.ts b/packages/@aws-cdk/aws-lambda/lib/code-signing-config.ts new file mode 100644 index 0000000000000..0472eb5d048f5 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/lib/code-signing-config.ts @@ -0,0 +1,120 @@ +import { ISigningProfile } from '@aws-cdk/aws-signer'; +import { IResource, Resource, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnCodeSigningConfig } from './lambda.generated'; + +/** + * Code signing configuration policy for deployment validation failure. + */ +export enum UntrustedArtifactOnDeployment { + /** + * Lambda blocks the deployment request if signature validation checks fail. + */ + ENFORCE = 'enforce', + + /** + * Lambda allows the deployment of the code package, but issues a warning. + * Lambda issues a new Amazon CloudWatch metric, called a signature validation error and also stores the warning in CloudTrail. + */ + WARN = 'warn', +} + +/** + * A Code Signing Config + */ +export interface ICodeSigningConfig extends IResource { + /** + * The ARN of Code Signing Config + * @attribute + */ + readonly codeSigningConfigArn: string; + + /** + * The id of Code Signing Config + * @attribute + */ + readonly codeSigningConfigId: string; +} + +/** + * Construction properties for a Code Signing Config object + */ +export interface CodeSigningConfigProps { + /** + * List of signing profiles that defines a + * trusted user who can sign a code package. + */ + readonly signingProfiles: ISigningProfile[], + + /** + * Code signing configuration policy for deployment validation failure. + * If you set the policy to Enforce, Lambda blocks the deployment request + * if signature validation checks fail. + * If you set the policy to Warn, Lambda allows the deployment and + * creates a CloudWatch log. + * + * @default UntrustedArtifactOnDeployment.WARN + */ + readonly untrustedArtifactOnDeployment?: UntrustedArtifactOnDeployment, + + /** + * Code signing configuration description. + * + * @default - No description. + */ + readonly description?: string, +} + +/** + * Defines a Code Signing Config. + * + * @resource AWS::Lambda::CodeSigningConfig + */ +export class CodeSigningConfig extends Resource implements ICodeSigningConfig { + /** + * Creates a Signing Profile construct that represents an external Signing Profile. + * + * @param scope The parent creating construct (usually `this`). + * @param id The construct's name. + * @param codeSigningConfigArn The ARN of code signing config. + */ + public static fromCodeSigningConfigArn( scope: Construct, id: string, codeSigningConfigArn: string): ICodeSigningConfig { + const codeSigningProfileId = Stack.of(scope).parseArn(codeSigningConfigArn).resourceName; + if (!codeSigningProfileId) { + throw new Error(`Code signing config ARN must be in the format 'arn:aws:lambda:::code-signing-config:', got: '${codeSigningConfigArn}'`); + } + const assertedCodeSigningProfileId = codeSigningProfileId; + class Import extends Resource implements ICodeSigningConfig { + public readonly codeSigningConfigArn = codeSigningConfigArn; + public readonly codeSigningConfigId = assertedCodeSigningProfileId; + + constructor() { + super(scope, id); + } + } + return new Import(); + } + + public readonly codeSigningConfigArn: string; + public readonly codeSigningConfigId: string; + + constructor(scope: Construct, id: string, props: CodeSigningConfigProps) { + super(scope, id); + + const signingProfileVersionArns = props.signingProfiles.map(signingProfile => { + return signingProfile.signingProfileVersionArn; + }); + + const resource: CfnCodeSigningConfig = new CfnCodeSigningConfig(this, 'Resource', { + allowedPublishers: { + signingProfileVersionArns, + }, + codeSigningPolicies: { + untrustedArtifactOnDeployment: props.untrustedArtifactOnDeployment ?? UntrustedArtifactOnDeployment.WARN, + }, + description: props.description, + }); + this.codeSigningConfigArn = resource.attrCodeSigningConfigArn; + this.codeSigningConfigId = resource.attrCodeSigningConfigId; + } +} diff --git a/packages/@aws-cdk/aws-lambda/lib/code.ts b/packages/@aws-cdk/aws-lambda/lib/code.ts index 29cd3d02ae4de..b4f41b2804257 100644 --- a/packages/@aws-cdk/aws-lambda/lib/code.ts +++ b/packages/@aws-cdk/aws-lambda/lib/code.ts @@ -57,6 +57,22 @@ export abstract class Code { return new AssetCode(path, options); } + /** + * Loads the function code from an asset created by a Docker build. + * + * By defaut, the asset is expected to be located at `/asset` in the + * image. + * + * @param path The path to the directory containing the Docker file + * @param options Docker build options + */ + public static fromDockerBuild(path: string, options: DockerBuildAssetOptions = {}): AssetCode { + const assetPath = cdk.DockerImage + .fromBuild(path, options) + .cp(options.imagePath ?? '/asset', options.outputPath); + return new AssetCode(assetPath); + } + /** * DEPRECATED * @deprecated use `fromAsset` @@ -488,3 +504,24 @@ export class AssetImageCode extends Code { }; } } + +/** + * Options when creating an asset from a Docker build. + */ +export interface DockerBuildAssetOptions extends cdk.DockerBuildOptions { + /** + * The path in the Docker image where the asset is located after the build + * operation. + * + * @default /asset + */ + readonly imagePath?: string; + + /** + * The path on the local filesystem where the asset will be copied + * using `docker cp`. + * + * @default - a unique temporary directory in the system temp directory + */ + readonly outputPath?: string; +} diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index fdcf4b4e0ec24..8d487276a6176 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -8,6 +8,7 @@ import * as sqs from '@aws-cdk/aws-sqs'; import { Annotations, CfnResource, Duration, Fn, Lazy, Names, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Code, CodeConfig } from './code'; +import { ICodeSigningConfig } from './code-signing-config'; import { EventInvokeConfigOptions } from './event-invoke-config'; import { IEventSource } from './event-source'; import { FileSystem } from './filesystem'; @@ -290,6 +291,13 @@ export interface FunctionOptions extends EventInvokeConfigOptions { * @default - AWS Lambda creates and uses an AWS managed customer master key (CMK). */ readonly environmentEncryption?: kms.IKey; + + /** + * Code signing config associated with this function + * + * @default - Not Sign the Code + */ + readonly codeSigningConfig?: ICodeSigningConfig; } export interface FunctionProps extends FunctionOptions { @@ -641,6 +649,7 @@ export class Function extends FunctionBase { }), kmsKeyArn: props.environmentEncryption?.keyArn, fileSystemConfigs, + codeSigningConfigArn: props.codeSigningConfig?.codeSigningConfigArn, }); resource.node.addDependency(this.role); diff --git a/packages/@aws-cdk/aws-lambda/lib/index.ts b/packages/@aws-cdk/aws-lambda/lib/index.ts index 1ba17427c5210..2d936755d6ad1 100644 --- a/packages/@aws-cdk/aws-lambda/lib/index.ts +++ b/packages/@aws-cdk/aws-lambda/lib/index.ts @@ -16,6 +16,7 @@ export * from './event-source-mapping'; export * from './destination'; export * from './event-invoke-config'; export * from './scalable-attribute-api'; +export * from './code-signing-config'; export * from './log-retention'; diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index de51f290aa4f5..6eab5cd11b870 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -99,6 +99,7 @@ "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", + "@aws-cdk/aws-signer": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cx-api": "0.0.0", @@ -119,6 +120,7 @@ "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", + "@aws-cdk/aws-signer": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cx-api": "0.0.0", @@ -169,7 +171,8 @@ "props-default-doc:@aws-cdk/aws-lambda.Permission.sourceArn", "docs-public-apis:@aws-cdk/aws-lambda.ResourceBindOptions", "docs-public-apis:@aws-cdk/aws-lambda.VersionAttributes", - "props-physical-name:@aws-cdk/aws-lambda.EventInvokeConfigProps" + "props-physical-name:@aws-cdk/aws-lambda.EventInvokeConfigProps", + "props-physical-name:@aws-cdk/aws-lambda.CodeSigningConfigProps" ] }, "stability": "stable", diff --git a/packages/@aws-cdk/aws-lambda/test/code-signing-config.test.ts b/packages/@aws-cdk/aws-lambda/test/code-signing-config.test.ts new file mode 100644 index 0000000000000..3e123ab5d5d89 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/code-signing-config.test.ts @@ -0,0 +1,102 @@ +import '@aws-cdk/assert/jest'; +import * as signer from '@aws-cdk/aws-signer'; +import * as cdk from '@aws-cdk/core'; +import * as lambda from '../lib'; + +let app: cdk.App; +let stack: cdk.Stack; +beforeEach( () => { + app = new cdk.App( {} ); + stack = new cdk.Stack( app ); +} ); + +describe('code signing config', () => { + test('default', () => { + const platform = signer.Platform.AWS_LAMBDA_SHA384_ECDSA; + const signingProfile = new signer.SigningProfile(stack, 'SigningProfile', { platform }); + new lambda.CodeSigningConfig(stack, 'CodeSigningConfig', { + signingProfiles: [signingProfile], + }); + + expect(stack).toHaveResource('AWS::Lambda::CodeSigningConfig', { + AllowedPublishers: { + SigningProfileVersionArns: [{ + 'Fn::GetAtt': [ + 'SigningProfile2139A0F9', + 'ProfileVersionArn', + ], + }], + }, + CodeSigningPolicies: { + UntrustedArtifactOnDeployment: lambda.UntrustedArtifactOnDeployment.WARN, + }, + }); + }); + + test('with multiple signing profiles', () => { + const signingProfile1 = new signer.SigningProfile(stack, 'SigningProfile1', { platform: signer.Platform.AWS_LAMBDA_SHA384_ECDSA }); + const signingProfile2 = new signer.SigningProfile(stack, 'SigningProfile2', { platform: signer.Platform.AMAZON_FREE_RTOS_DEFAULT }); + const signingProfile3 = new signer.SigningProfile(stack, 'SigningProfile3', { platform: signer.Platform.AWS_IOT_DEVICE_MANAGEMENT_SHA256_ECDSA }); + new lambda.CodeSigningConfig(stack, 'CodeSigningConfig', { + signingProfiles: [signingProfile1, signingProfile2, signingProfile3], + }); + + expect(stack).toHaveResource('AWS::Lambda::CodeSigningConfig', { + AllowedPublishers: { + SigningProfileVersionArns: [ + { + 'Fn::GetAtt': [ + 'SigningProfile1D4191686', + 'ProfileVersionArn', + ], + }, + { + 'Fn::GetAtt': [ + 'SigningProfile2E013C934', + 'ProfileVersionArn', + ], + }, + { + 'Fn::GetAtt': [ + 'SigningProfile3A38DE231', + 'ProfileVersionArn', + ], + }, + ], + }, + }); + }); + + test('with description and with untrustedArtifactOnDeployment of "ENFORCE"', () => { + const platform = signer.Platform.AWS_LAMBDA_SHA384_ECDSA; + const signingProfile = new signer.SigningProfile(stack, 'SigningProfile', { platform }); + new lambda.CodeSigningConfig(stack, 'CodeSigningConfig', { + signingProfiles: [signingProfile], + untrustedArtifactOnDeployment: lambda.UntrustedArtifactOnDeployment.ENFORCE, + description: 'test description', + }); + + expect(stack).toHaveResource('AWS::Lambda::CodeSigningConfig', { + CodeSigningPolicies: { + UntrustedArtifactOnDeployment: lambda.UntrustedArtifactOnDeployment.ENFORCE, + }, + Description: 'test description', + }); + }); + + test('import does not create any resources', () => { + const codeSigningConfigId = 'aaa-xxxxxxxxxx'; + const codeSigningConfigArn = `arn:aws:lambda:::code-signing-config:${codeSigningConfigId}`; + const codeSigningConfig = lambda.CodeSigningConfig.fromCodeSigningConfigArn(stack, 'Imported', codeSigningConfigArn ); + + expect(codeSigningConfig.codeSigningConfigArn).toBe(codeSigningConfigArn); + expect(codeSigningConfig.codeSigningConfigId).toBe(codeSigningConfigId); + expect(stack).toCountResources('AWS::Lambda::CodeSigningConfig', 0); + }); + + test('fail import with malformed code signing config arn', () => { + const codeSigningConfigArn = 'arn:aws:lambda:::code-signing-config'; + + expect(() => lambda.CodeSigningConfig.fromCodeSigningConfigArn(stack, 'Imported', codeSigningConfigArn ) ).toThrow(/ARN must be in the format/); + }); +}); diff --git a/packages/@aws-cdk/aws-lambda/test/code.test.ts b/packages/@aws-cdk/aws-lambda/test/code.test.ts index 9b99c095c2467..91de07a17c5a6 100644 --- a/packages/@aws-cdk/aws-lambda/test/code.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/code.test.ts @@ -4,6 +4,7 @@ import { ABSENT, ResourcePart } from '@aws-cdk/assert'; import * as ecr from '@aws-cdk/aws-ecr'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as lambda from '../lib'; /* eslint-disable dot-notation */ @@ -275,9 +276,10 @@ describe('code', () => { }); describe('lambda.Code.fromImageAsset', () => { - test('repository uri is correctly identified', () => { + const flags = { [cxapi.DOCKER_IGNORE_SUPPORT]: true }; + testFutureBehavior('repository uri is correctly identified', flags, cdk.App, (app) => { // given - const stack = new cdk.Stack(); + const stack = new cdk.Stack(app); // when new lambda.Function(stack, 'Fn', { @@ -296,7 +298,7 @@ describe('code', () => { { Ref: 'AWS::Region' }, '.', { Ref: 'AWS::URLSuffix' }, - '/aws-cdk/assets:0874c7dfd254e95f5181cc7fa643e4abf010f68e5717e373b6e635b49a115b2b', + '/aws-cdk/assets:e8a944aeb0a08ba4811503d9c138e514b112dadca84daa5b4608e4a0fb80a0c9', ]], }, }, @@ -327,6 +329,29 @@ describe('code', () => { }); }); }); + + describe('lambda.Code.fromDockerBuild', () => { + test('can use the result of a Docker build as an asset', () => { + // given + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); + + // when + new lambda.Function(stack, 'Fn', { + code: lambda.Code.fromDockerBuild(path.join(__dirname, 'docker-build-lambda')), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_12_X, + }); + + // then + expect(stack).toHaveResource('AWS::Lambda::Function', { + Metadata: { + [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.38cd320fa97b348accac88e48d9cede4923f7cab270ce794c95a665be83681a8', + [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code', + }, + }, ResourcePart.CompleteDefinition); + }); + }); }); function defineFunction(code: lambda.Code, runtime: lambda.Runtime = lambda.Runtime.NODEJS_10_X) { diff --git a/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile new file mode 100644 index 0000000000000..4643fde141850 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile @@ -0,0 +1,3 @@ +FROM public.ecr.aws/amazonlinux/amazonlinux:latest + +COPY index.js /asset diff --git a/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/index.ts b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/index.ts new file mode 100644 index 0000000000000..cc867895b4efc --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/index.ts @@ -0,0 +1,5 @@ +/* eslint-disable no-console */ +export async function handler(event: any) { + console.log('Event: %j', event); + return event; +} diff --git a/packages/@aws-cdk/aws-lambda/test/function.test.ts b/packages/@aws-cdk/aws-lambda/test/function.test.ts index 51cfe70fd878c..50cf6b0c9b72b 100644 --- a/packages/@aws-cdk/aws-lambda/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/function.test.ts @@ -9,6 +9,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as sqs from '@aws-cdk/aws-sqs'; +import * as signer from '@aws-cdk/aws-signer'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; import * as _ from 'lodash'; @@ -2003,6 +2004,36 @@ describe('function', () => { }); }); }); + + describe('code signing config', () => { + test('default', () => { + const stack = new cdk.Stack(); + + const signingProfile = new signer.SigningProfile(stack, 'SigningProfile', { + platform: signer.Platform.AWS_LAMBDA_SHA384_ECDSA, + }); + + const codeSigningConfig = new lambda.CodeSigningConfig(stack, 'CodeSigningConfig', { + signingProfiles: [signingProfile], + }); + + new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_10_X, + codeSigningConfig, + }); + + expect(stack).toHaveResource('AWS::Lambda::Function', { + CodeSigningConfigArn: { + 'Fn::GetAtt': [ + 'CodeSigningConfigD8D41C10', + 'CodeSigningConfigArn', + ], + }, + }); + }); + }); }); function newTestLambda(scope: constructs.Construct) { diff --git a/packages/@aws-cdk/aws-logs/package.json b/packages/@aws-cdk/aws-logs/package.json index f5a004e27bb1f..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.7", + "nock": "^13.0.10", "nodeunit": "^0.11.3", "pkglint": "0.0.0", "sinon": "^9.2.4" diff --git a/packages/@aws-cdk/aws-logs/test/test.log-retention-provider.ts b/packages/@aws-cdk/aws-logs/test/test.log-retention-provider.ts index b13eebe9aabe2..a08ff060dc2a4 100644 --- a/packages/@aws-cdk/aws-logs/test/test.log-retention-provider.ts +++ b/packages/@aws-cdk/aws-logs/test/test.log-retention-provider.ts @@ -208,7 +208,7 @@ export = { }, async 'responds with FAILED on error'(test: Test) { - const createLogGroupFake = sinon.fake.rejects(new Error('UnkownError')); + const createLogGroupFake = sinon.fake.rejects(new Error('UnknownError')); AWS.mock('CloudWatchLogs', 'createLogGroup', createLogGroupFake); diff --git a/packages/@aws-cdk/aws-neptune/README.md b/packages/@aws-cdk/aws-neptune/README.md index 6b2eddde67362..fc542acf1b3da 100644 --- a/packages/@aws-cdk/aws-neptune/README.md +++ b/packages/@aws-cdk/aws-neptune/README.md @@ -9,10 +9,101 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- -```ts +Amazon Neptune is a fast, reliable, fully managed graph database service that makes it easy to build and run applications that work with highly connected datasets. The core of Neptune is a purpose-built, high-performance graph database engine. This engine is optimized for storing billions of relationships and querying the graph with milliseconds latency. Neptune supports the popular graph query languages Apache TinkerPop Gremlin and W3C’s SPARQL, enabling you to build queries that efficiently navigate highly connected datasets. + +The `@aws-cdk/aws-neptune` package contains primitives for setting up Neptune database clusters and instances. + +```ts nofixture import * as neptune from '@aws-cdk/aws-neptune'; ``` + +## Starting a Neptune Database + +To set up a Neptune database, define a `DatabaseCluster`. You must always launch a database in a VPC. + +```ts +const cluster = new neptune.DatabaseCluster(this, 'Database', { + vpc, + instanceType: neptune.InstanceType.R5_LARGE +}); +``` + +By default only writer instance is provisioned with this construct. + +## Connecting + +To control who can access the cluster, use the `.connections` attribute. Neptune databases have a default port, so +you don't need to specify the port: + +```ts fixture=with-cluster +cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); +``` + +The endpoints to access your database cluster will be available as the `.clusterEndpoint` and `.clusterReadEndpoint` +attributes: + +```ts fixture=with-cluster +const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" +``` + +## Customizing parameters + +Neptune allows configuring database behavior by supplying custom parameter groups. For more details, refer to the +following link: + +```ts +const clusterParams = new neptune.ClusterParameterGroup(this, 'ClusterParams', { + description: 'Cluster parameter group', + parameters: { + neptune_enable_audit_log: '1' + }, +}); + +const dbParams = new neptune.ParameterGroup(this, 'DbParams', { + description: 'Db parameter group', + parameters: { + neptune_query_timeout: '120000' + }, +}); + +const cluster = new neptune.DatabaseCluster(this, 'Database', { + vpc, + instanceType: neptune.InstanceType.R5_LARGE, + clusterParameterGroup: clusterParams, + parameterGroup: dbParams, +}); +``` + +## Adding replicas + +`DatabaseCluster` allows launching replicas along with the writer instance. This can be specified using the `instanceCount` +attribute. + +```ts +const cluster = new neptune.DatabaseCluster(this, 'Database', { + vpc, + instanceType: neptune.InstanceType.R5_LARGE, + instances: 2 +}); +``` + +Additionally it is also possible to add replicas using `DatabaseInstance` for an existing cluster. + +```ts fixture=with-cluster +const replica1 = new neptune.DatabaseInstance(this, 'Instance', { + cluster, + instanceType: neptune.InstanceType.R5_LARGE +}); +``` diff --git a/packages/@aws-cdk/aws-neptune/lib/cluster.ts b/packages/@aws-cdk/aws-neptune/lib/cluster.ts new file mode 100644 index 0000000000000..4adbe2ce8ea04 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/cluster.ts @@ -0,0 +1,457 @@ +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 { Construct } from 'constructs'; +import { Endpoint } from './endpoint'; +import { InstanceType } from './instance'; +import { CfnDBCluster, CfnDBInstance } from './neptune.generated'; +import { IClusterParameterGroup, IParameterGroup } from './parameter-group'; +import { ISubnetGroup, SubnetGroup } from './subnet-group'; + +/** + * Possible Instances Types to use in Neptune cluster + * used for defining {@link DatabaseClusterProps.engineVersion}. + */ +export class EngineVersion { + /** + * Neptune engine version 1.0.1.0 + */ + public static readonly V1_0_1_0 = new EngineVersion('1.0.1.0'); + /** + * Neptune engine version 1.0.1.1 + */ + public static readonly V1_0_1_1 = new EngineVersion('1.0.1.1'); + /** + * Neptune engine version 1.0.1.2 + */ + public static readonly V1_0_1_2 = new EngineVersion('1.0.1.2'); + /** + * Neptune engine version 1.0.2.1 + */ + public static readonly V1_0_2_1 = new EngineVersion('1.0.2.1'); + /** + * Neptune engine version 1.0.2.2 + */ + public static readonly V1_0_2_2 = new EngineVersion('1.0.2.2'); + /** + * Neptune engine version 1.0.3.0 + */ + public static readonly V1_0_3_0 = new EngineVersion('1.0.3.0'); + /** + * Neptune engine version 1.0.4.0 + */ + public static readonly V1_0_4_0 = new EngineVersion('1.0.4.0'); + /** + * Neptune engine version 1.0.4.1 + */ + public static readonly V1_0_4_1 = new EngineVersion('1.0.4.1'); + + /** + * Constructor for specifying a custom engine version + * @param version the engine version of Neptune + */ + public constructor(public readonly version: string) {} +} + +/** + * Properties for a new database cluster + */ +export interface DatabaseClusterProps { + /** + * What version of the database to start + * + * @default - The default engine version. + */ + readonly engineVersion?: EngineVersion; + + /** + * The port the Neptune cluster will listen on + * + * @default - The default engine port + */ + readonly port?: number; + + /** + * How many days to retain the backup + * + * @default - cdk.Duration.days(1) + */ + readonly backupRetention?: Duration; + + /** + * A daily time range in 24-hours UTC format in which backups preferably execute. + * + * Must be at least 30 minutes long. + * + * Example: '01:00-02:00' + * + * @default - a 30-minute window selected at random from an 8-hour block of + * time for each AWS Region. To see the time blocks available, see + */ + readonly preferredBackupWindow?: string; + + /** + * The KMS key for storage encryption. + * + * @default - default master key. + */ + readonly kmsKey?: kms.IKey; + + /** + * Whether to enable storage encryption + * + * @default true + */ + readonly storageEncrypted?: boolean; + + /** + * Number of Neptune compute instances + * + * @default 1 + */ + readonly instances?: number; + + /** + * An optional identifier for the cluster + * + * @default - A name is automatically generated. + */ + readonly dbClusterName?: string; + + /** + * Base identifier for instances + * + * Every replica is named by appending the replica number to this string, 1-based. + * + * @default - `dbClusterName` is used with the word "Instance" appended. If `dbClusterName` is not provided, the + * identifier is automatically generated. + */ + readonly instanceIdentifierBase?: string; + + /** + * What type of instance to start for the replicas + */ + readonly instanceType: InstanceType; + + /** + * A list of AWS Identity and Access Management (IAM) role that can be used by the cluster to access other AWS services. + * + * @default - No role is attached to the cluster. + */ + readonly associatedRoles?: iam.IRole[]; + + /** + * Indicates whether the DB cluster should have deletion protection enabled. + * + * @default - true if ``removalPolicy`` is RETAIN, false otherwise + */ + readonly deletionProtection?: boolean; + + /** + * A weekly time range in which maintenance should preferably execute. + * + * Must be at least 30 minutes long. + * + * Example: 'tue:04:17-tue:04:47' + * + * @default - 30-minute window selected at random from an 8-hour block of time for + * each AWS Region, occurring on a random day of the week. + */ + readonly preferredMaintenanceWindow?: string; + + /** + * Additional parameters to pass to the database engine + * + * @default - No parameter group. + */ + readonly clusterParameterGroup?: IClusterParameterGroup; + + /** + * The DB parameter group to associate with the instance. + * + * @default no parameter group + */ + readonly parameterGroup?: IParameterGroup; + + /** + * Existing subnet group for the cluster. + * + * @default - a new subnet group will be created. + */ + readonly subnetGroup?: ISubnetGroup; + + /** + * What subnets to run the Neptune instances in. + * + * Must be at least 2 subnets in two different AZs. + */ + readonly vpc: ec2.IVpc; + + /** + * Where to place the instances within the VPC + * + * @default private subnets + */ + readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * Security group. + * + * @default a new security group is created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * The removal policy to apply when the cluster and its instances are removed + * or replaced during a stack update, or when the stack is deleted. This + * removal policy also applies to the implicit security group created for the + * cluster if one is not supplied as a parameter. + * + * @default - Retain cluster. + */ + readonly removalPolicy?: RemovalPolicy +} + +/** + * Create a clustered database with a given number of instances. + */ +export interface IDatabaseCluster extends IResource, ec2.IConnectable { + /** + * Identifier of the cluster + */ + readonly clusterIdentifier: string; + + /** + * The endpoint to use for read/write operations + * @attribute Endpoint,Port + */ + readonly clusterEndpoint: Endpoint; + + /** + * Endpoint to use for load-balanced read-only operations. + * @attribute ReadEndpoint + */ + readonly clusterReadEndpoint: Endpoint; +} + +/** + * Properties that describe an existing cluster instance + */ +export interface DatabaseClusterAttributes { + /** + * The database port + */ + readonly port: number; + + /** + * The security group of the database cluster + */ + readonly securityGroup: ec2.ISecurityGroup; + + /** + * Identifier for the cluster + */ + readonly clusterIdentifier: string; + + /** + * Cluster endpoint address + */ + readonly clusterEndpointAddress: string; + + /** + * Reader endpoint address + */ + readonly readerEndpointAddress: string; +} + +/** + * Create a clustered database with a given number of instances. + * + * @resource AWS::Neptune::DBCluster + */ +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; + + /** + * Import an existing DatabaseCluster from properties + */ + public static fromDatabaseClusterAttributes(scope: Construct, id: string, attrs: DatabaseClusterAttributes): IDatabaseCluster { + class Import extends Resource implements IDatabaseCluster { + public readonly defaultPort = ec2.Port.tcp(attrs.port); + public readonly connections = new ec2.Connections({ + securityGroups: [attrs.securityGroup], + defaultPort: this.defaultPort, + }); + public readonly clusterIdentifier = attrs.clusterIdentifier; + public readonly clusterEndpoint = new Endpoint(attrs.clusterEndpointAddress, attrs.port); + public readonly clusterReadEndpoint = new Endpoint(attrs.readerEndpointAddress, attrs.port); + } + + return new Import(scope, id); + } + + /** + * Identifier of the cluster + */ + public readonly clusterIdentifier: string; + + /** + * The endpoint to use for read/write operations + */ + public readonly clusterEndpoint: Endpoint; + + /** + * Endpoint to use for load-balanced read-only operations. + */ + public readonly clusterReadEndpoint: Endpoint; + + /** + * The resource id for the cluster; for example: cluster-ABCD1234EFGH5678IJKL90MNOP. The cluster ID uniquely + * identifies the cluster and is used in things like IAM authentication policies. + * @attribute ClusterResourceId + */ + public readonly clusterResourceIdentifier: string; + + /** + * The connections object to implement IConectable + */ + public readonly connections: ec2.Connections; + + /** + * The VPC where the DB subnet group is created. + */ + public readonly vpc: ec2.IVpc; + + /** + * The subnets used by the DB subnet group. + */ + public readonly vpcSubnets: ec2.SubnetSelection; + + /** + * Subnet group used by the DB + */ + public readonly subnetGroup: ISubnetGroup; + + /** + * Identifiers of the instance + */ + public readonly instanceIdentifiers: string[] = []; + + /** + * Endpoints which address each individual instance. + */ + public readonly instanceEndpoints: Endpoint[] = []; + + constructor(scope: Construct, id: string, props: DatabaseClusterProps) { + super(scope, id); + + this.vpc = props.vpc; + this.vpcSubnets = props.vpcSubnets ?? { subnetType: ec2.SubnetType.PRIVATE }; + + // Determine the subnet(s) to deploy the Neptune cluster to + const { subnetIds, internetConnectivityEstablished } = this.vpc.selectSubnets(this.vpcSubnets); + + // Cannot test whether the subnets are in different AZs, but at least we can test the amount. + if (subnetIds.length < 2) { + throw new Error(`Cluster requires at least 2 subnets, got ${subnetIds.length}`); + } + + this.subnetGroup = props.subnetGroup ?? new SubnetGroup(this, 'Subnets', { + description: `Subnets for ${id} database`, + vpc: this.vpc, + vpcSubnets: this.vpcSubnets, + removalPolicy: props.removalPolicy === RemovalPolicy.RETAIN ? props.removalPolicy : undefined, + }); + + const securityGroups = props.securityGroups ?? [ + new ec2.SecurityGroup(this, 'SecurityGroup', { + description: 'Neptune security group', + vpc: this.vpc, + }), + ]; + + // Default to encrypted storage + const storageEncrypted = props.storageEncrypted ?? true; + + if (props.kmsKey && !storageEncrypted) { + throw new Error('KMS key supplied but storageEncrypted is false'); + } + + const deletionProtection = props.deletionProtection ?? (props.removalPolicy === RemovalPolicy.RETAIN ? true : undefined); + + // Create the Neptune cluster + const cluster = new CfnDBCluster(this, 'Resource', { + // Basic + engineVersion: props.engineVersion?.version, + dbClusterIdentifier: props.dbClusterName, + dbSubnetGroupName: this.subnetGroup.subnetGroupName, + port: props.port, + vpcSecurityGroupIds: securityGroups.map(sg => sg.securityGroupId), + dbClusterParameterGroupName: props.clusterParameterGroup?.clusterParameterGroupName, + deletionProtection: deletionProtection, + associatedRoles: props.associatedRoles ? props.associatedRoles.map(role => ({ roleArn: role.roleArn })) : undefined, + // Backup + backupRetentionPeriod: props.backupRetention?.toDays(), + preferredBackupWindow: props.preferredBackupWindow, + preferredMaintenanceWindow: props.preferredMaintenanceWindow, + // Encryption + kmsKeyId: props.kmsKey?.keyArn, + storageEncrypted, + }); + + cluster.applyRemovalPolicy(props.removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + + this.clusterIdentifier = cluster.ref; + this.clusterResourceIdentifier = cluster.attrClusterResourceId; + + const port = Token.asNumber(cluster.attrPort); + this.clusterEndpoint = new Endpoint(cluster.attrEndpoint, port); + this.clusterReadEndpoint = new Endpoint(cluster.attrReadEndpoint, port); + + // Create the instances + const instanceCount = props.instances ?? DatabaseCluster.DEFAULT_NUM_INSTANCES; + if (instanceCount < 1) { + throw new Error('At least one instance is required'); + } + + for (let i = 0; i < instanceCount; i++) { + const instanceIndex = i + 1; + + const instanceIdentifier = props.instanceIdentifierBase != null ? `${props.instanceIdentifierBase}${instanceIndex}` + : props.dbClusterName != null ? `${props.dbClusterName}instance${instanceIndex}` : undefined; + + const instance = new CfnDBInstance(this, `Instance${instanceIndex}`, { + // Link to cluster + dbClusterIdentifier: cluster.ref, + dbInstanceIdentifier: instanceIdentifier, + // Instance properties + dbInstanceClass: props.instanceType, + dbParameterGroupName: props.parameterGroup?.parameterGroupName, + }); + + // We must have a dependency on the NAT gateway provider here to create + // things in the right order. + instance.node.addDependency(internetConnectivityEstablished); + + instance.applyRemovalPolicy(props.removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + + this.instanceIdentifiers.push(instance.ref); + this.instanceEndpoints.push(new Endpoint(instance.attrEndpoint, port)); + } + + this.connections = new ec2.Connections({ + defaultPort: ec2.Port.tcp(port), + securityGroups: securityGroups, + }); + } +} diff --git a/packages/@aws-cdk/aws-neptune/lib/endpoint.ts b/packages/@aws-cdk/aws-neptune/lib/endpoint.ts new file mode 100644 index 0000000000000..fc92942416b7d --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/endpoint.ts @@ -0,0 +1,31 @@ +import { Token } from '@aws-cdk/core'; + +/** + * Connection endpoint of a neptune cluster or instance + * + * Consists of a combination of hostname and port. + */ +export class Endpoint { + /** + * The hostname of the endpoint + */ + public readonly hostname: string; + + /** + * The port of the endpoint + */ + public readonly port: number; + + /** + * The combination of "HOSTNAME:PORT" for this endpoint + */ + public readonly socketAddress: string; + + constructor(address: string, port: number) { + this.hostname = address; + this.port = port; + + const portDesc = Token.isUnresolved(port) ? Token.asString(port) : port; + this.socketAddress = `${address}:${portDesc}`; + } +} diff --git a/packages/@aws-cdk/aws-neptune/lib/index.ts b/packages/@aws-cdk/aws-neptune/lib/index.ts index 67cdd432ee7d2..35257958b20aa 100644 --- a/packages/@aws-cdk/aws-neptune/lib/index.ts +++ b/packages/@aws-cdk/aws-neptune/lib/index.ts @@ -1,2 +1,8 @@ +export * from './cluster'; +export * from './instance'; +export * from './endpoint'; +export * from './parameter-group'; +export * from './subnet-group'; + // AWS::Neptune CloudFormation Resources: export * from './neptune.generated'; diff --git a/packages/@aws-cdk/aws-neptune/lib/instance.ts b/packages/@aws-cdk/aws-neptune/lib/instance.ts new file mode 100644 index 0000000000000..8459c710577c8 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/instance.ts @@ -0,0 +1,234 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IDatabaseCluster } from './cluster'; +import { Endpoint } from './endpoint'; +import { CfnDBInstance } from './neptune.generated'; +import { IParameterGroup } from './parameter-group'; + +/** + * Possible Instances Types to use in Neptune cluster + * used for defining {@link DatabaseInstanceProps.instanceType}. + */ +export enum InstanceType { + /** + * db.r5.large + */ + R5_LARGE = 'db.r5.large', + /** + * db.r5.xlarge + */ + R5_XLARGE = 'db.r5.xlarge', + /** + * db.r5.2xlarge + */ + R5_2XLARGE = 'db.r5.2xlarge', + /** + * db.r5.4xlarge + */ + R5_4XLARGE = 'db.r5.4xlarge', + /** + * db.r5.8xlarge + */ + R5_8XLARGE = 'db.r5.8xlarge', + /** + * db.r5.12xlarge + */ + R5_12XLARGE = 'db.r5.12xlarge', + /** + * db.r5.24xlarge + */ + R5_24XLARGE = 'db.r5.24xlarge', + /** + * db.r4.large + */ + R4_LARGE = 'db.r4.large', + /** + * db.r4.xlarge + */ + R4_XLARGE = 'db.r4.xlarge', + /** + * db.r4.2xlarge + */ + R4_2XLARGE = 'db.r4.2xlarge', + /** + * db.r4.4xlarge + */ + R4_4XLARGE = 'db.r4.4xlarge', + /** + * db.r4.8xlarge + */ + R4_8XLARGE = 'db.r4.8xlarge', + /** + * db.t3.medium + */ + T3_MEDIUM = 'db.t3.medium' +} + +/** + * A database instance + */ +export interface IDatabaseInstance extends cdk.IResource { + /** + * The instance identifier. + */ + readonly instanceIdentifier: string; + + /** + * The instance endpoint. + */ + readonly instanceEndpoint: Endpoint; + + /** + * The instance endpoint address. + * + * @attribute Endpoint + */ + readonly dbInstanceEndpointAddress: string; + + /** + * The instance endpoint port. + * + * @attribute Port + */ + readonly dbInstanceEndpointPort: string; +} + +/** + * Properties that describe an existing instance + */ +export interface DatabaseInstanceAttributes { + /** + * The instance identifier. + */ + readonly instanceIdentifier: string; + + /** + * The endpoint address. + */ + readonly instanceEndpointAddress: string; + + /** + * The database port. + */ + readonly port: number; +} + +/** + * Construction properties for a DatabaseInstanceNew + */ +export interface DatabaseInstanceProps { + /** + * The Neptune database cluster the instance should launch into. + */ + readonly cluster: IDatabaseCluster; + + /** + * What type of instance to start for the replicas + */ + readonly instanceType: InstanceType; + + /** + * The name of the Availability Zone where the DB instance will be located. + * + * @default - no preference + */ + readonly availabilityZone?: string; + + /** + * A name for the DB instance. If you specify a name, AWS CloudFormation + * converts it to lowercase. + * + * @default - a CloudFormation generated name + */ + readonly dbInstanceName?: string; + + /** + * The DB parameter group to associate with the instance. + * + * @default no parameter group + */ + readonly parameterGroup?: IParameterGroup; + + /** + * The CloudFormation policy to apply when the instance is removed from the + * stack or replaced during an update. + * + * @default RemovalPolicy.Retain + */ + readonly removalPolicy?: cdk.RemovalPolicy +} + +/** + * A database instance + * + * @resource AWS::Neptune::DBInstance + */ +export class DatabaseInstance extends cdk.Resource implements IDatabaseInstance { + + /** + * Import an existing database instance. + */ + public static fromDatabaseInstanceAttributes(scope: Construct, id: string, attrs: DatabaseInstanceAttributes): IDatabaseInstance { + class Import extends cdk.Resource implements IDatabaseInstance { + public readonly defaultPort = ec2.Port.tcp(attrs.port); + public readonly instanceIdentifier = attrs.instanceIdentifier; + public readonly dbInstanceEndpointAddress = attrs.instanceEndpointAddress; + public readonly dbInstanceEndpointPort = attrs.port.toString(); + public readonly instanceEndpoint = new Endpoint(attrs.instanceEndpointAddress, attrs.port); + } + + return new Import(scope, id); + } + + + /** + * The instance's database cluster + */ + public readonly cluster: IDatabaseCluster; + + /** + * @inheritdoc + */ + public readonly instanceIdentifier: string; + + /** + * @inheritdoc + */ + public readonly instanceEndpoint: Endpoint; + + /** + * @inheritdoc + */ + public readonly dbInstanceEndpointAddress: string; + + /** + * @inheritdoc + */ + public readonly dbInstanceEndpointPort: string; + + constructor(scope: Construct, id: string, props: DatabaseInstanceProps) { + super(scope, id); + + const instance = new CfnDBInstance(this, 'Resource', { + dbClusterIdentifier: props.cluster.clusterIdentifier, + dbInstanceClass: props.instanceType, + availabilityZone: props.availabilityZone, + dbInstanceIdentifier: props.dbInstanceName, + dbParameterGroupName: props.parameterGroup?.parameterGroupName, + }); + + this.cluster = props.cluster; + this.instanceIdentifier = instance.ref; + this.dbInstanceEndpointAddress = instance.attrEndpoint; + this.dbInstanceEndpointPort = instance.attrPort; + + // create a number token that represents the port of the instance + const portAttribute = cdk.Token.asNumber(instance.attrPort); + this.instanceEndpoint = new Endpoint(instance.attrEndpoint, portAttribute); + + instance.applyRemovalPolicy(props.removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + } +} diff --git a/packages/@aws-cdk/aws-neptune/lib/parameter-group.ts b/packages/@aws-cdk/aws-neptune/lib/parameter-group.ts new file mode 100644 index 0000000000000..3cfacf061f19c --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/parameter-group.ts @@ -0,0 +1,135 @@ +import { IResource, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnDBClusterParameterGroup } from './neptune.generated'; + +/** + * Properties for a parameter group + */ +interface ParameterGroupPropsBase { + /** + * Description for this parameter group + * + * @default a CDK generated description + */ + readonly description?: string; + + /** + * The parameters in this parameter group + */ + readonly parameters: { [key: string]: string }; +} + +/** + * Marker class for cluster parameter group + */ +export interface ClusterParameterGroupProps extends ParameterGroupPropsBase { + /** + * The name of the parameter group + * + * @default A CDK generated name for the parameter group + */ + readonly clusterParameterGroupName?: string; +} + +/** + * Marker class for cluster parameter group + */ +export interface ParameterGroupProps extends ParameterGroupPropsBase { + /** + * The name of the parameter group + * + * @default A CDK generated name for the parameter group + */ + readonly parameterGroupName?: string; +} + +/** + * A parameter group + */ +export interface IClusterParameterGroup extends IResource { + /** + * The name of this parameter group + */ + readonly clusterParameterGroupName: string; +} + + +/** + * A cluster parameter group + * + * @resource AWS::Neptune::DBClusterParameterGroup + */ +export class ClusterParameterGroup extends Resource implements IClusterParameterGroup { + /** + * Imports a parameter group + */ + public static fromClusterParameterGroupName(scope: Construct, id: string, clusterParameterGroupName: string): IClusterParameterGroup { + class Import extends Resource implements IClusterParameterGroup { + public readonly clusterParameterGroupName = clusterParameterGroupName; + } + return new Import(scope, id); + } + + /** + * The name of the parameter group + */ + public readonly clusterParameterGroupName: string; + + constructor(scope: Construct, id: string, props: ClusterParameterGroupProps) { + super(scope, id); + + const resource = new CfnDBClusterParameterGroup(this, 'Resource', { + name: props.clusterParameterGroupName, + description: props.description || 'Cluster parameter group for neptune db cluster', + family: 'neptune1', + parameters: props.parameters, + }); + + this.clusterParameterGroupName = resource.ref; + } +} + +/** + * A parameter group + */ +export interface IParameterGroup extends IResource { + /** + * The name of this parameter group + */ + readonly parameterGroupName: string; +} + +/** + * DB parameter group + * + * @resource AWS::Neptune::DBParameterGroup + */ +export class ParameterGroup extends Resource implements IParameterGroup { + /** + * Imports a parameter group + */ + public static fromParameterGroupName(scope: Construct, id: string, parameterGroupName: string): IParameterGroup { + class Import extends Resource implements IParameterGroup { + public readonly parameterGroupName = parameterGroupName; + } + return new Import(scope, id); + } + + /** + * The name of the parameter group + */ + public readonly parameterGroupName: string; + + constructor(scope: Construct, id: string, props: ParameterGroupProps) { + super(scope, id); + + const resource = new CfnDBClusterParameterGroup(this, 'Resource', { + name: props.parameterGroupName, + description: props.description || 'Instance parameter group for neptune db instances', + family: 'neptune1', + parameters: props.parameters, + }); + + this.parameterGroupName = resource.ref; + } +} diff --git a/packages/@aws-cdk/aws-neptune/lib/subnet-group.ts b/packages/@aws-cdk/aws-neptune/lib/subnet-group.ts new file mode 100644 index 0000000000000..383435b7a0b38 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/lib/subnet-group.ts @@ -0,0 +1,91 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import { IResource, RemovalPolicy, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnDBSubnetGroup } from './neptune.generated'; + +/** + * Interface for a subnet group. + */ +export interface ISubnetGroup extends IResource { + /** + * The name of the subnet group. + * @attribute + */ + readonly subnetGroupName: string; +} + +/** + * Properties for creating a SubnetGroup. + */ +export interface SubnetGroupProps { + /** + * Description of the subnet group. + * + * @default - a name is generated + */ + readonly description?: string; + + /** + * The VPC to place the subnet group in. + */ + readonly vpc: ec2.IVpc; + + /** + * The name of the subnet group. + * + * @default - a name is generated + */ + readonly subnetGroupName?: string; + + /** + * Which subnets within the VPC to associate with this group. + * + * @default - private subnets + */ + readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * The removal policy to apply when the subnet group are removed + * from the stack or replaced during an update. + * + * @default RemovalPolicy.DESTROY + */ + readonly removalPolicy?: RemovalPolicy +} + +/** + * Class for creating a RDS DB subnet group + * + * @resource AWS::Neptune::DBSubnetGroup + */ +export class SubnetGroup extends Resource implements ISubnetGroup { + + /** + * Imports an existing subnet group by name. + */ + public static fromSubnetGroupName(scope: Construct, id: string, subnetGroupName: string): ISubnetGroup { + return new class extends Resource implements ISubnetGroup { + public readonly subnetGroupName = subnetGroupName; + }(scope, id); + } + + public readonly subnetGroupName: string; + + constructor(scope: Construct, id: string, props: SubnetGroupProps) { + super(scope, id); + + const { subnetIds } = props.vpc.selectSubnets(props.vpcSubnets ?? { subnetType: ec2.SubnetType.PRIVATE }); + + const subnetGroup = new CfnDBSubnetGroup(this, 'Resource', { + dbSubnetGroupDescription: props.description || 'Subnet group for Neptune', + dbSubnetGroupName: props.subnetGroupName, + subnetIds, + }); + + if (props.removalPolicy) { + subnetGroup.applyRemovalPolicy(props.removalPolicy); + } + + this.subnetGroupName = subnetGroup.ref; + } +} diff --git a/packages/@aws-cdk/aws-neptune/package.json b/packages/@aws-cdk/aws-neptune/package.json index f4ae26436389b..5afe6d91322a2 100644 --- a/packages/@aws-cdk/aws-neptune/package.json +++ b/packages/@aws-cdk/aws-neptune/package.json @@ -74,22 +74,36 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.2.0" }, "peerDependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.2.0" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, + "awslint": { + "exclude": [ + "props-physical-name:@aws-cdk/aws-neptune.ParameterGroupProps", + "props-physical-name:@aws-cdk/aws-neptune.ClusterParameterGroupProps", + "props-physical-name:@aws-cdk/aws-neptune.SubnetGroupProps" + ] + }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..2e687290371fa --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/rosetta/default.ts-fixture @@ -0,0 +1,14 @@ +import { Duration, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as neptune from '@aws-cdk/aws-neptune'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const vpc = new ec2.Vpc(this, 'VPC', { maxAzs: 2 }); + + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/rosetta/with-cluster.ts-fixture b/packages/@aws-cdk/aws-neptune/rosetta/with-cluster.ts-fixture new file mode 100644 index 0000000000000..c638d8b4d04fa --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/rosetta/with-cluster.ts-fixture @@ -0,0 +1,19 @@ +import { Duration, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as neptune from '@aws-cdk/aws-neptune'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const vpc = new ec2.Vpc(this, 'VPC', { maxAzs: 2 }); + + const cluster = new neptune.DatabaseCluster(this, 'Database', { + vpc, + instanceType: neptune.InstanceType.R5_LARGE, + }); + + /// here + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/test/cluster.test.ts b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts new file mode 100644 index 0000000000000..d2c5ff4b6c1ef --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/cluster.test.ts @@ -0,0 +1,439 @@ +import { expect as expectCDK, haveResource, 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'; +import * as cdk from '@aws-cdk/core'; + +import { ClusterParameterGroup, DatabaseCluster, EngineVersion, InstanceType } from '../lib'; + +describe('DatabaseCluster', () => { + + test('check that instantiation works', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + Properties: { + DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, + VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], + StorageEncrypted: true, + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + + expectCDK(stack).to(haveResource('AWS::Neptune::DBSubnetGroup', { + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + { Ref: 'VPCPrivateSubnet3Subnet3EDCD457' }, + ], + })); + }); + + test('can create a cluster with a single instance', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + instances: 1, + vpc, + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, + VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], + })); + }); + + test('errors when less than one instance is specified', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + expect(() => { + new DatabaseCluster(stack, 'Database', { + instances: 0, + vpc, + instanceType: InstanceType.R5_LARGE, + }); + }).toThrowError('At least one instance is required'); + }); + + test('errors when only one subnet is specified', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC', { + maxAzs: 1, + }); + + // WHEN + expect(() => { + new DatabaseCluster(stack, 'Database', { + instances: 1, + vpc, + vpcSubnets: { + subnetType: ec2.SubnetType.PRIVATE, + }, + instanceType: InstanceType.R5_LARGE, + }); + }).toThrowError('Cluster requires at least 2 subnets, got 1'); + }); + + test('can create a cluster with custom engine version', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + engineVersion: EngineVersion.V1_0_4_1, + }); + + // THEN + expectCDK(stack).to(haveResource('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', () => { + // GIVEN + const stack = testStack(); + const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { + vpcId: 'VPC12345', + }); + const sg = ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'SecurityGroupId12345'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + instances: 1, + vpc, + securityGroups: [sg], + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + DBSubnetGroupName: { Ref: 'DatabaseSubnets3C9252C9' }, + VpcSecurityGroupIds: ['SecurityGroupId12345'], + })); + }); + + test('cluster with parameter group', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const group = new ClusterParameterGroup(stack, 'Params', { + description: 'bye', + parameters: { + param: 'value', + }, + }); + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + clusterParameterGroup: group, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + DBClusterParameterGroupName: { Ref: 'ParamsA8366201' }, + })); + }); + + test('cluster with associated role', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('rds.amazonaws.com'), + }); + role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonS3ReadOnlyAccess')); + + new DatabaseCluster(stack, 'Database', { + vpc, + associatedRoles: [role], + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + AssociatedRoles: [ + { + RoleArn: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], + }, + }, + ], + })); + }); + + test('cluster with imported parameter group', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const group = ClusterParameterGroup.fromClusterParameterGroupName(stack, 'Params', 'ParamGroupName'); + + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + clusterParameterGroup: group, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + DBClusterParameterGroupName: 'ParamGroupName', + })); + }); + + test('create an encrypted cluster with custom KMS key', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + kmsKey: new kms.Key(stack, 'Key'), + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + KmsKeyId: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + StorageEncrypted: true, + })); + }); + + test('creating a cluster defaults to using encryption', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + StorageEncrypted: true, + })); + }); + + test('supplying a KMS key with storageEncryption false throws an error', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + function action() { + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + kmsKey: new kms.Key(stack, 'Key'), + storageEncrypted: false, + }); + } + + // THEN + expect(action).toThrow(); + }); + + test('cluster exposes different read and write endpoints', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const cluster = new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expect(stack.resolve(cluster.clusterEndpoint)).not.toBe(stack.resolve(cluster.clusterReadEndpoint)); + }); + + test('instance identifier used when present', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const instanceIdentifierBase = 'instanceidentifierbase-'; + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + instanceIdentifierBase, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + DBInstanceIdentifier: `${instanceIdentifierBase}1`, + })); + }); + + test('cluster identifier used', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const clusterIdentifier = 'clusteridentifier-'; + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + dbClusterName: clusterIdentifier, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + DBInstanceIdentifier: `${clusterIdentifier}instance1`, + })); + }); + + test('imported cluster has supplied attributes', () => { + // GIVEN + const stack = testStack(); + + // WHEN + const cluster = DatabaseCluster.fromDatabaseClusterAttributes(stack, 'Database', { + clusterEndpointAddress: 'addr', + clusterIdentifier: 'identifier', + port: 3306, + readerEndpointAddress: 'reader-address', + securityGroup: ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { + allowAllOutbound: false, + }), + }); + + // THEN + expect(cluster.clusterEndpoint.hostname).toEqual('addr'); + expect(cluster.clusterEndpoint.port).toEqual(3306); + expect(cluster.clusterIdentifier).toEqual('identifier'); + expect(cluster.clusterReadEndpoint.hostname).toEqual('reader-address'); + }); + + test('imported cluster with imported security group honors allowAllOutbound', () => { + // GIVEN + const stack = testStack(); + + const cluster = DatabaseCluster.fromDatabaseClusterAttributes(stack, 'Database', { + clusterEndpointAddress: 'addr', + clusterIdentifier: 'identifier', + port: 3306, + readerEndpointAddress: 'reader-address', + securityGroup: ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { + allowAllOutbound: false, + }), + }); + + // WHEN + cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); + + // THEN + expectCDK(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + GroupId: 'sg-123456789', + })); + }); + + test('backup retention period respected', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + backupRetention: cdk.Duration.days(20), + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + BackupRetentionPeriod: 20, + })); + }); + + test('backup maintenance window respected', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + backupRetention: cdk.Duration.days(20), + preferredBackupWindow: '07:34-08:04', + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + BackupRetentionPeriod: 20, + PreferredBackupWindow: '07:34-08:04', + })); + }); + + test('regular maintenance window respected', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + vpc, + instanceType: InstanceType.R5_LARGE, + preferredMaintenanceWindow: '07:34-08:04', + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBCluster', { + PreferredMaintenanceWindow: '07:34-08:04', + })); + }); +}); + +function testStack() { + const stack = new cdk.Stack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); + stack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); + return stack; +} diff --git a/packages/@aws-cdk/aws-neptune/test/endpoint.test.ts b/packages/@aws-cdk/aws-neptune/test/endpoint.test.ts new file mode 100644 index 0000000000000..cd5bd17bd3af2 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/endpoint.test.ts @@ -0,0 +1,15 @@ +import { Endpoint } from '../lib'; + +describe('Endpoint', () => { + test('accepts valid port string numbers', () => { + // GIVEN + for (const port of [1, 50, 65535]) { + // WHEN + const endpoint = new Endpoint('127.0.0.1', port); + + // THEN + expect(endpoint.port).toBe(port); + } + }); + +}); diff --git a/packages/@aws-cdk/aws-neptune/test/instance.test.ts b/packages/@aws-cdk/aws-neptune/test/instance.test.ts new file mode 100644 index 0000000000000..4dcfc75e243ac --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/instance.test.ts @@ -0,0 +1,131 @@ +import { expect as expectCDK, haveOutput, haveResource, ResourcePart } from '@aws-cdk/assert'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as constructs from 'constructs'; + +import { DatabaseCluster, DatabaseInstance, InstanceType, ParameterGroup } from '../lib'; + +describe('DatabaseInstance', () => { + test('check that instantiation works', () => { + // GIVEN + const stack = testStack(); + + // WHEN + new DatabaseInstance(stack, 'Instance', { + cluster: stack.cluster, + instanceType: InstanceType.R5_LARGE, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + Properties: { + DBClusterIdentifier: { Ref: 'DatabaseB269D8BB' }, + DBInstanceClass: 'db.r5.large', + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + }); + + test('check that the endpoint works', () => { + // GIVEN + const stack = testStack(); + const instance = new DatabaseInstance(stack, 'Instance', { + cluster: stack.cluster, + instanceType: InstanceType.R5_LARGE, + }); + const exportName = 'DbInstanceEndpoint'; + + // WHEN + new cdk.CfnOutput(stack, exportName, { + exportName, + value: instance.instanceEndpoint.socketAddress, + }); + + // THEN + expectCDK(stack).to(haveOutput({ + exportName, + outputValue: { + 'Fn::Join': [ + '', + [ + { 'Fn::GetAtt': ['InstanceC1063A87', 'Endpoint'] }, + ':', + { 'Fn::GetAtt': ['InstanceC1063A87', 'Port'] }, + ], + ], + }, + })); + }); + + test('check importing works as expected', () => { + // GIVEN + const stack = testStack(); + const endpointExportName = 'DbInstanceEndpoint'; + const instanceEndpointAddress = '127.0.0.1'; + const instanceIdentifier = 'InstanceID'; + const port = 8888; + + // WHEN + const instance = DatabaseInstance.fromDatabaseInstanceAttributes(stack, 'Instance', { + instanceEndpointAddress, + instanceIdentifier, + port, + }); + new cdk.CfnOutput(stack, 'EndpointOutput', { + exportName: endpointExportName, + value: instance.instanceEndpoint.socketAddress, + }); + + // THEN + expectCDK(stack).to(haveOutput({ + exportName: endpointExportName, + outputValue: `${instanceEndpointAddress}:${port}`, + })); + }); + + test('instance with parameter group', () => { + // GIVEN + const stack = testStack(); + + // WHEN + const group = new ParameterGroup(stack, 'Params', { + description: 'bye', + parameters: { + param: 'value', + }, + }); + new DatabaseInstance(stack, 'Instance', { + cluster: stack.cluster, + instanceType: InstanceType.R5_LARGE, + parameterGroup: group, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Neptune::DBInstance', { + DBParameterGroupName: { Ref: 'ParamsA8366201' }, + })); + }); +}); + +class TestStack extends cdk.Stack { + public readonly vpc: ec2.Vpc; + public readonly cluster: DatabaseCluster; + + constructor(scope?: constructs.Construct, id?: string, props: cdk.StackProps = {}) { + super(scope, id, props); + + this.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); + + this.vpc = new ec2.Vpc(this, 'VPC'); + this.cluster = new DatabaseCluster(this, 'Database', { + instanceType: InstanceType.R5_LARGE, + vpc: this.vpc, + }); + } +} + +function testStack() { + const stack = new TestStack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); + return stack; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json b/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json new file mode 100644 index 0000000000000..823f7af2a5b45 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/integ.cluster.expected.json @@ -0,0 +1,504 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "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": "aws-cdk-neptune-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/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": "aws-cdk-neptune-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "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": "aws-cdk-neptune-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/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": "aws-cdk-neptune-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "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": "aws-cdk-neptune-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/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.192.0/18", + "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": "aws-cdk-neptune-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/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" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-neptune-integ/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "ParamsA8366201": { + "Type": "AWS::Neptune::DBClusterParameterGroup", + "Properties": { + "Description": "A nice parameter group", + "Family": "neptune1", + "Parameters": { + "neptune_enable_audit_log": "1", + "neptune_query_timeout": "100000" + } + } + }, + "DbSecurity381C2C15": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DatabaseSubnets3C9252C9": { + "Type": "AWS::Neptune::DBSubnetGroup", + "Properties": { + "DBSubnetGroupDescription": "Subnets for Database database", + "SubnetIds": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + ] + } + }, + "DatabaseSecurityGroup5C91FDCB": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Neptune security group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "DatabaseSecurityGroupfrom00000IndirectPortF24F2E03": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "CidrIp": "0.0.0.0/0", + "Description": "Open to the world", + "FromPort": { + "Fn::GetAtt": [ + "DatabaseB269D8BB", + "Port" + ] + }, + "GroupId": { + "Fn::GetAtt": [ + "DatabaseSecurityGroup5C91FDCB", + "GroupId" + ] + }, + "ToPort": { + "Fn::GetAtt": [ + "DatabaseB269D8BB", + "Port" + ] + } + } + }, + "DatabaseB269D8BB": { + "Type": "AWS::Neptune::DBCluster", + "Properties": { + "DBClusterParameterGroupName": { + "Ref": "ParamsA8366201" + }, + "DBSubnetGroupName": { + "Ref": "DatabaseSubnets3C9252C9" + }, + "KmsKeyId": { + "Fn::GetAtt": [ + "DbSecurity381C2C15", + "Arn" + ] + }, + "StorageEncrypted": true, + "VpcSecurityGroupIds": [ + { + "Fn::GetAtt": [ + "DatabaseSecurityGroup5C91FDCB", + "GroupId" + ] + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DatabaseInstance1844F58FD": { + "Type": "AWS::Neptune::DBInstance", + "Properties": { + "DBInstanceClass": "db.r5.large", + "DBClusterIdentifier": { + "Ref": "DatabaseB269D8BB" + } + }, + "DependsOn": [ + "VPCPrivateSubnet1DefaultRouteAE1D6490", + "VPCPrivateSubnet2DefaultRouteF4F5CFD2" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts b/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts new file mode 100644 index 0000000000000..b62c0d054a624 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/integ.cluster.ts @@ -0,0 +1,48 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as kms from '@aws-cdk/aws-kms'; +import * as cdk from '@aws-cdk/core'; +import * as constructs from 'constructs'; +import { DatabaseCluster, InstanceType } from '../lib'; +import { ClusterParameterGroup } from '../lib/parameter-group'; + +/* + * Stack verification steps: + * * aws docdb describe-db-clusters --db-cluster-identifier + */ + +class TestStack extends cdk.Stack { + constructor(scope: constructs.Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const vpc = new ec2.Vpc(this, 'VPC', { maxAzs: 2 }); + + const params = new ClusterParameterGroup(this, 'Params', { + description: 'A nice parameter group', + parameters: { + neptune_enable_audit_log: '1', + neptune_query_timeout: '100000', + }, + }); + + const kmsKey = new kms.Key(this, 'DbSecurity', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const cluster = new DatabaseCluster(this, 'Database', { + vpc, + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, + instanceType: InstanceType.R5_LARGE, + clusterParameterGroup: params, + kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); + } +} + +const app = new cdk.App(); + +new TestStack(app, 'aws-cdk-neptune-integ'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-neptune/test/neptune.test.ts b/packages/@aws-cdk/aws-neptune/test/neptune.test.ts deleted file mode 100644 index e394ef336bfb4..0000000000000 --- a/packages/@aws-cdk/aws-neptune/test/neptune.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assert/jest'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts b/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts new file mode 100644 index 0000000000000..6ba6ff6562d76 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/parameter-group.test.ts @@ -0,0 +1,50 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import { ClusterParameterGroup, ParameterGroup } from '../lib'; + +describe('ClusterParameterGroup', () => { + + test('create a cluster parameter group', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new ClusterParameterGroup(stack, 'Params', { + description: 'desc', + parameters: { + key: 'value', + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::Neptune::DBClusterParameterGroup', { + Description: 'desc', + Parameters: { + key: 'value', + }, + })); + + }); + + test('create a instance/db parameter group', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new ParameterGroup(stack, 'Params', { + description: 'desc', + parameters: { + key: 'value', + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::Neptune::DBClusterParameterGroup', { + Description: 'desc', + Parameters: { + key: 'value', + }, + })); + + }); +}); diff --git a/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts b/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts new file mode 100644 index 0000000000000..e6c75013716c8 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/test/subnet-group.test.ts @@ -0,0 +1,83 @@ +import '@aws-cdk/assert/jest'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import { Stack } from '@aws-cdk/core'; +import { SubnetGroup } from '../lib'; + +let stack: Stack; +let vpc: ec2.IVpc; + +beforeEach(() => { + stack = new Stack(); + vpc = new ec2.Vpc(stack, 'VPC'); +}); + +test('creates a subnet group from minimal properties', () => { + new SubnetGroup(stack, 'Group', { + description: 'MyGroup', + vpc, + }); + + expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + DBSubnetGroupDescription: 'MyGroup', + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + ], + }); +}); + +test('creates a subnet group from all properties', () => { + new SubnetGroup(stack, 'Group', { + description: 'My Shared Group', + subnetGroupName: 'SharedGroup', + vpc, + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE }, + }); + + expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + DBSubnetGroupDescription: 'My Shared Group', + DBSubnetGroupName: 'SharedGroup', + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + ], + }); +}); + +describe('subnet selection', () => { + test('defaults to private subnets', () => { + new SubnetGroup(stack, 'Group', { + description: 'MyGroup', + vpc, + }); + + expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + DBSubnetGroupDescription: 'MyGroup', + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + ], + }); + }); + + test('can specify subnet type', () => { + new SubnetGroup(stack, 'Group', { + description: 'MyGroup', + vpc, + vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, + }); + + expect(stack).toHaveResource('AWS::Neptune::DBSubnetGroup', { + DBSubnetGroupDescription: 'MyGroup', + SubnetIds: [ + { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, + { Ref: 'VPCPublicSubnet2Subnet74179F39' }, + ], + }); + }); +}); + +test('import group by name', () => { + const subnetGroup = SubnetGroup.fromSubnetGroupName(stack, 'Group', 'my-subnet-group'); + expect(subnetGroup.subnetGroupName).toBe('my-subnet-group'); +}); diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index b00e3c64187f0..cb431cecbcd10 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -525,7 +525,7 @@ const cluster = new rds.ServerlessCluster(this, 'AnotherCluster', { }); const fn = new lambda.Function(this, 'MyFunction', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), environment: { diff --git a/packages/@aws-cdk/aws-rds/package.json b/packages/@aws-cdk/aws-rds/package.json index 01ce0a8ca4a12..685b5c399b240 100644 --- a/packages/@aws-cdk/aws-rds/package.json +++ b/packages/@aws-cdk/aws-rds/package.json @@ -74,6 +74,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-events-targets": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-rds/test/cluster.test.ts b/packages/@aws-cdk/aws-rds/test/cluster.test.ts index 6bfda0f1cfa50..11e60d8068341 100644 --- a/packages/@aws-cdk/aws-rds/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-rds/test/cluster.test.ts @@ -1,18 +1,20 @@ -import { ABSENT, countResources, expect, haveResource, haveResourceLike, ResourcePart, SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { ABSENT, ResourcePart, SynthUtils } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import { ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; +import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import { AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, CfnDBCluster, Credentials, DatabaseCluster, DatabaseClusterEngine, DatabaseClusterFromSnapshot, ParameterGroup, PerformanceInsightRetention, SubnetGroup, } from '../lib'; -nodeunitShim({ - 'creating a Cluster also creates 2 DB Instances'(test: Test) { +describe('cluster', () => { + test('creating a Cluster also creates 2 DB Instances', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -31,7 +33,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { Properties: { Engine: 'aurora', DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, @@ -41,18 +43,18 @@ nodeunitShim({ }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - expect(stack).to(countResources('AWS::RDS::DBInstance', 2)); - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toCountResources('AWS::RDS::DBInstance', 2); + expect(stack).toHaveResource('AWS::RDS::DBInstance', { DeletionPolicy: 'Delete', UpdateReplacePolicy: 'Delete', - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - test.done(); - }, - 'can create a cluster with a single instance'(test: Test) { + }); + + test('can create a cluster with a single instance', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -72,18 +74,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { Engine: 'aurora', DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, MasterUsername: 'admin', MasterUserPassword: 'tooshort', VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId'] }], - })); + }); - test.done(); - }, - 'can create a cluster with imported vpc and security group'(test: Test) { + }); + + test('can create a cluster with imported vpc and security group', () => { // GIVEN const stack = testStack(); const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { @@ -107,18 +109,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { Engine: 'aurora', DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, MasterUsername: 'admin', MasterUserPassword: 'tooshort', VpcSecurityGroupIds: ['SecurityGroupId12345'], - })); + }); - test.done(); - }, - 'cluster with parameter group'(test: Test) { + }); + + test('cluster with parameter group', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -145,14 +147,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { DBClusterParameterGroupName: { Ref: 'ParamsA8366201' }, - })); + }); - test.done(); - }, - "sets the retention policy of the SubnetGroup to 'Retain' if the Cluster is created with 'Retain'"(test: Test) { + }); + + test("sets the retention policy of the SubnetGroup to 'Retain' if the Cluster is created with 'Retain'", () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); @@ -166,15 +168,15 @@ nodeunitShim({ removalPolicy: cdk.RemovalPolicy.RETAIN, }); - expect(stack).to(haveResourceLike('AWS::RDS::DBSubnetGroup', { + expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - test.done(); - }, - 'creates a secret when master credentials are not specified'(test: Test) { + }); + + test('creates a secret when master credentials are not specified', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -193,7 +195,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { MasterUsername: { 'Fn::Join': [ '', @@ -218,21 +220,21 @@ nodeunitShim({ ], ], }, - })); + }); - expect(stack).to(haveResource('AWS::SecretsManager::Secret', { + expect(stack).toHaveResource('AWS::SecretsManager::Secret', { GenerateSecretString: { ExcludeCharacters: '\"@/\\', GenerateStringKey: 'password', PasswordLength: 30, SecretStringTemplate: '{"username":"admin"}', }, - })); + }); - test.done(); - }, - 'create an encrypted cluster with custom KMS key'(test: Test) { + }); + + test('create an encrypted cluster with custom KMS key', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -251,19 +253,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { KmsKeyId: { 'Fn::GetAtt': [ 'Key961B73FD', 'Arn', ], }, - })); + }); - test.done(); - }, - 'cluster with instance parameter group'(test: Test) { + }); + + test('cluster with instance parameter group', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -287,18 +289,17 @@ nodeunitShim({ }, }); - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { DBParameterGroupName: { Ref: 'ParameterGroup5E32DECB', }, - })); + }); - test.done(); - }, + }); - 'performance insights': { - 'cluster with all performance insights properties'(test: Test) { + describe('performance insights', () => { + test('cluster with all performance insights properties', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -317,16 +318,16 @@ nodeunitShim({ }, }); - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { EnablePerformanceInsights: true, PerformanceInsightsRetentionPeriod: 731, PerformanceInsightsKMSKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, - })); + }); - test.done(); - }, - 'setting performance insights fields enables performance insights'(test: Test) { + }); + + test('setting performance insights fields enables performance insights', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -343,20 +344,20 @@ nodeunitShim({ }, }); - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { EnablePerformanceInsights: true, PerformanceInsightsRetentionPeriod: 731, - })); + }); - test.done(); - }, - 'throws if performance insights fields are set but performance insights is disabled'(test: Test) { + }); + + test('throws if performance insights fields are set but performance insights is disabled', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); - test.throws(() => { + expect(() => { new DatabaseCluster(stack, 'Database', { engine: DatabaseClusterEngine.AURORA, credentials: { @@ -368,13 +369,13 @@ nodeunitShim({ performanceInsightRetention: PerformanceInsightRetention.DEFAULT, }, }); - }, /`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set/); + }).toThrow(/`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set/); - test.done(); - }, - }, - 'cluster with disable automatic upgrade of minor version'(test: Test) { + }); + }); + + test('cluster with disable automatic upgrade of minor version', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -388,14 +389,14 @@ nodeunitShim({ }, }); - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { AutoMinorVersionUpgrade: false, - })); + }); - test.done(); - }, - 'cluster with allow upgrade of major version'(test: Test) { + }); + + test('cluster with allow upgrade of major version', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -409,14 +410,14 @@ nodeunitShim({ }, }); - expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { AllowMajorVersionUpgrade: true, - })); + }); - test.done(); - }, - 'cluster with disallow remove backups'(test: Test) { + }); + + test('cluster with disallow remove backups', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -430,14 +431,14 @@ nodeunitShim({ }, }); - expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { DeleteAutomatedBackups: false, - })); + }); - test.done(); - }, - 'create a cluster using a specific version of MySQL'(test: Test) { + }); + + test('create a cluster using a specific version of MySQL', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -457,15 +458,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { Engine: 'aurora-mysql', EngineVersion: '5.7.mysql_aurora.2.04.4', - })); + }); - test.done(); - }, - 'create a cluster using a specific version of Postgresql'(test: Test) { + }); + + test('create a cluster using a specific version of Postgresql', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -485,15 +486,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { Engine: 'aurora-postgresql', EngineVersion: '10.7', - })); + }); + - test.done(); - }, + }); - 'cluster exposes different read and write endpoints'(test: Test) { + test('cluster exposes different read and write endpoints', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -511,15 +512,12 @@ nodeunitShim({ }); // THEN - test.notDeepEqual( - stack.resolve(cluster.clusterEndpoint), - stack.resolve(cluster.clusterReadEndpoint), - ); + expect(stack.resolve(cluster.clusterEndpoint)).not.toEqual(stack.resolve(cluster.clusterReadEndpoint)); - test.done(); - }, - 'imported cluster with imported security group honors allowAllOutbound'(test: Test) { + }); + + test('imported cluster with imported security group honors allowAllOutbound', () => { // GIVEN const stack = testStack(); @@ -539,41 +537,41 @@ nodeunitShim({ cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', - })); + }); - test.done(); - }, - 'can import a cluster with minimal attributes'(test: Test) { + }); + + test('can import a cluster with minimal attributes', () => { const stack = testStack(); const cluster = DatabaseCluster.fromDatabaseClusterAttributes(stack, 'Database', { clusterIdentifier: 'identifier', }); - test.equals(cluster.clusterIdentifier, 'identifier'); + expect(cluster.clusterIdentifier).toEqual('identifier'); + - test.done(); - }, + }); - 'minimal imported cluster throws on accessing attributes for unprovided parameters'(test: Test) { + test('minimal imported cluster throws on accessing attributes for unprovided parameters', () => { const stack = testStack(); const cluster = DatabaseCluster.fromDatabaseClusterAttributes(stack, 'Database', { clusterIdentifier: 'identifier', }); - test.throws(() => cluster.clusterEndpoint, /Cannot access `clusterEndpoint` of an imported cluster/); - test.throws(() => cluster.clusterReadEndpoint, /Cannot access `clusterReadEndpoint` of an imported cluster/); - test.throws(() => cluster.instanceIdentifiers, /Cannot access `instanceIdentifiers` of an imported cluster/); - test.throws(() => cluster.instanceEndpoints, /Cannot access `instanceEndpoints` of an imported cluster/); + expect(() => cluster.clusterEndpoint).toThrow(/Cannot access `clusterEndpoint` of an imported cluster/); + expect(() => cluster.clusterReadEndpoint).toThrow(/Cannot access `clusterReadEndpoint` of an imported cluster/); + expect(() => cluster.instanceIdentifiers).toThrow(/Cannot access `instanceIdentifiers` of an imported cluster/); + expect(() => cluster.instanceEndpoints).toThrow(/Cannot access `instanceEndpoints` of an imported cluster/); + - test.done(); - }, + }); - 'imported cluster can access properties if attributes are provided'(test: Test) { + test('imported cluster can access properties if attributes are provided', () => { const stack = testStack(); const cluster = DatabaseCluster.fromDatabaseClusterAttributes(stack, 'Database', { @@ -588,15 +586,15 @@ nodeunitShim({ })], }); - test.equals(cluster.clusterEndpoint.socketAddress, 'addr:3306'); - test.equals(cluster.clusterReadEndpoint.socketAddress, 'reader-address:3306'); - test.deepEqual(cluster.instanceIdentifiers, ['identifier']); - test.deepEqual(cluster.instanceEndpoints.map(endpoint => endpoint.socketAddress), ['instance-addr:3306']); + expect(cluster.clusterEndpoint.socketAddress).toEqual('addr:3306'); + expect(cluster.clusterReadEndpoint.socketAddress).toEqual('reader-address:3306'); + expect(cluster.instanceIdentifiers).toEqual(['identifier']); + expect(cluster.instanceEndpoints.map(endpoint => endpoint.socketAddress)).toEqual(['instance-addr:3306']); - test.done(); - }, - 'cluster supports metrics'(test: Test) { + }); + + test('cluster supports metrics', () => { const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -611,7 +609,7 @@ nodeunitShim({ }, }); - test.deepEqual(stack.resolve(cluster.metricCPUUtilization()), { + expect(stack.resolve(cluster.metricCPUUtilization())).toEqual({ dimensions: { DBClusterIdentifier: { Ref: 'DatabaseB269D8BB' } }, namespace: 'AWS/RDS', metricName: 'CPUUtilization', @@ -621,10 +619,10 @@ nodeunitShim({ region: 'us-test-1', }); - test.done(); - }, - 'cluster with enabled monitoring'(test: Test) { + }); + + test('cluster with enabled monitoring', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -644,14 +642,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { MonitoringInterval: 60, MonitoringRoleArn: { 'Fn::GetAtt': ['DatabaseMonitoringRole576991DA', 'Arn'], }, - }, ResourcePart.Properties)); + }, ResourcePart.Properties); - expect(stack).to(haveResource('AWS::IAM::Role', { + expect(stack).toHaveResource('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -678,12 +676,12 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'create a cluster with imported monitoring role'(test: Test) { + }); + + test('create a cluster with imported monitoring role', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -711,17 +709,17 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { MonitoringInterval: 60, MonitoringRoleArn: { 'Fn::GetAtt': ['MonitoringRole90457BF9', 'Arn'], }, - }, ResourcePart.Properties)); + }, ResourcePart.Properties); + - test.done(); - }, + }); - 'throws when trying to add rotation to a cluster without secret'(test: Test) { + test('throws when trying to add rotation to a cluster without secret', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -740,12 +738,12 @@ nodeunitShim({ }); // THEN - test.throws(() => cluster.addRotationSingleUser(), /without secret/); + expect(() => cluster.addRotationSingleUser()).toThrow(/without secret/); - test.done(); - }, - 'throws when trying to add single user rotation multiple times'(test: Test) { + }); + + test('throws when trying to add single user rotation multiple times', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -762,12 +760,12 @@ nodeunitShim({ cluster.addRotationSingleUser(); // THEN - test.throws(() => cluster.addRotationSingleUser(), /A single user rotation was already added to this cluster/); + expect(() => cluster.addRotationSingleUser()).toThrow(/A single user rotation was already added to this cluster/); - test.done(); - }, - 'create a cluster with s3 import role'(test: Test) { + }); + + test('create a cluster with s3 import role', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -791,7 +789,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -800,9 +798,9 @@ nodeunitShim({ ], }, }], - })); + }); - expect(stack).to(haveResource('AWS::RDS::DBClusterParameterGroup', { + expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_load_from_s3_role: { @@ -812,12 +810,12 @@ nodeunitShim({ ], }, }, - })); + }); + - test.done(); - }, + }); - 'create a cluster with s3 import buckets'(test: Test) { + test('create a cluster with s3 import buckets', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -839,7 +837,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -848,9 +846,9 @@ nodeunitShim({ ], }, }], - })); + }); - expect(stack).to(haveResource('AWS::RDS::DBClusterParameterGroup', { + expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_load_from_s3_role: { @@ -860,9 +858,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -898,12 +896,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - test.done(); - }, - 'cluster with s3 import bucket adds supported feature name to IAM role'(test: Test) { + }); + + test('cluster with s3 import bucket adds supported feature name to IAM role', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -927,7 +925,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -937,12 +935,12 @@ nodeunitShim({ }, FeatureName: 's3Import', }], - })); + }); - test.done(); - }, - 'throws when s3 import bucket or s3 export bucket is supplied for a Postgres version that does not support it'(test: Test) { + }); + + test('throws when s3 import bucket or s3 export bucket is supplied for a Postgres version that does not support it', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -950,7 +948,7 @@ nodeunitShim({ const bucket = new s3.Bucket(stack, 'Bucket'); // WHEN / THEN - test.throws(() => { + expect(() => { new DatabaseCluster(stack, 'Database', { engine: DatabaseClusterEngine.auroraPostgres({ version: AuroraPostgresEngineVersion.VER_10_4, @@ -965,9 +963,9 @@ nodeunitShim({ }, s3ImportBuckets: [bucket], }); - }, /s3Import is not supported for Postgres version: 10.4. Use a version that supports the s3Import feature./); + }).toThrow(/s3Import is not supported for Postgres version: 10.4. Use a version that supports the s3Import feature./); - test.throws(() => { + expect(() => { new DatabaseCluster(stack, 'AnotherDatabase', { engine: DatabaseClusterEngine.auroraPostgres({ version: AuroraPostgresEngineVersion.VER_10_4, @@ -982,12 +980,12 @@ nodeunitShim({ }, s3ExportBuckets: [bucket], }); - }, /s3Export is not supported for Postgres version: 10.4. Use a version that supports the s3Export feature./); + }).toThrow(/s3Export is not supported for Postgres version: 10.4. Use a version that supports the s3Export feature./); + - test.done(); - }, + }); - 'cluster with s3 export bucket adds supported feature name to IAM role'(test: Test) { + test('cluster with s3 export bucket adds supported feature name to IAM role', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1011,7 +1009,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1021,12 +1019,12 @@ nodeunitShim({ }, FeatureName: 's3Export', }], - })); + }); - test.done(); - }, - 'create a cluster with s3 export role'(test: Test) { + }); + + test('create a cluster with s3 export role', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1050,7 +1048,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1059,9 +1057,9 @@ nodeunitShim({ ], }, }], - })); + }); - expect(stack).to(haveResource('AWS::RDS::DBClusterParameterGroup', { + expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_select_into_s3_role: { @@ -1071,14 +1069,14 @@ nodeunitShim({ ], }, }, - })); + }); - test.done(); - }, - 'create a cluster with s3 export buckets'(test: Test) { + }); + + testFutureBehavior('create a cluster with s3 export buckets', { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }, cdk.App, (app) => { // GIVEN - const stack = testStack(); + const stack = testStack(app); const vpc = new ec2.Vpc(stack, 'VPC'); const bucket = new s3.Bucket(stack, 'Bucket'); @@ -1098,7 +1096,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1107,9 +1105,9 @@ nodeunitShim({ ], }, }], - })); + }); - expect(stack).to(haveResource('AWS::RDS::DBClusterParameterGroup', { + expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_select_into_s3_role: { @@ -1119,9 +1117,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -1130,7 +1128,7 @@ nodeunitShim({ 's3:GetBucket*', 's3:List*', 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], Effect: 'Allow', @@ -1160,12 +1158,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); + - test.done(); - }, + }); - 'create a cluster with s3 import and export buckets'(test: Test) { + test('create a cluster with s3 import and export buckets', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1189,7 +1187,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1206,9 +1204,9 @@ nodeunitShim({ ], }, }], - })); + }); - expect(stack).to(haveResource('AWS::RDS::DBClusterParameterGroup', { + expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { aurora_load_from_s3_role: { @@ -1224,12 +1222,12 @@ nodeunitShim({ ], }, }, - })); + }); + - test.done(); - }, + }); - 'create a cluster with s3 import and export buckets and custom parameter group'(test: Test) { + test('create a cluster with s3 import and export buckets and custom parameter group', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1261,7 +1259,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1278,9 +1276,9 @@ nodeunitShim({ ], }, }], - })); + }); - expect(stack).to(haveResource('AWS::RDS::DBClusterParameterGroup', { + expect(stack).toHaveResource('AWS::RDS::DBClusterParameterGroup', { Family: 'aurora5.6', Parameters: { key: 'value', @@ -1297,12 +1295,12 @@ nodeunitShim({ ], }, }, - })); + }); - test.done(); - }, - 'PostgreSQL cluster with s3 export buckets does not generate custom parameter group and specifies the correct port'(test: Test) { + }); + + test('PostgreSQL cluster with s3 export buckets does not generate custom parameter group and specifies the correct port', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1326,7 +1324,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::RDS::DBCluster', { + expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { AssociatedRoles: [{ RoleArn: { 'Fn::GetAtt': [ @@ -1337,14 +1335,14 @@ nodeunitShim({ }], DBClusterParameterGroupName: 'default.aurora-postgresql11', Port: 5432, - })); + }); - expect(stack).notTo(haveResource('AWS::RDS::DBClusterParameterGroup')); + expect(stack).not.toHaveResource('AWS::RDS::DBClusterParameterGroup'); - test.done(); - }, - 'unversioned PostgreSQL cluster can be used with s3 import and s3 export buckets'(test: Test) { + }); + + test('unversioned PostgreSQL cluster can be used with s3 import and s3 export buckets', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1368,7 +1366,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { AssociatedRoles: [ { FeatureName: 's3Import', @@ -1389,12 +1387,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - "Aurora PostgreSQL cluster uses a different default master username than 'admin', which is a reserved word"(test: Test) { + }); + + test("Aurora PostgreSQL cluster uses a different default master username than 'admin', which is a reserved word", () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1408,16 +1406,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::SecretsManager::Secret', { + expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { GenerateSecretString: { SecretStringTemplate: '{"username":"postgres"}', }, - })); + }); - test.done(); - }, - 'MySQL cluster without S3 exports or imports references the correct default ParameterGroup'(test: Test) { + }); + + test('MySQL cluster without S3 exports or imports references the correct default ParameterGroup', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1436,16 +1434,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::RDS::DBCluster', { + expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { DBClusterParameterGroupName: 'default.aurora-mysql5.7', - })); + }); + + expect(stack).not.toHaveResource('AWS::RDS::DBClusterParameterGroup'); - expect(stack).notTo(haveResource('AWS::RDS::DBClusterParameterGroup')); - test.done(); - }, + }); - 'throws when s3ExportRole and s3ExportBuckets properties are both specified'(test: Test) { + test('throws when s3ExportRole and s3ExportBuckets properties are both specified', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1456,7 +1454,7 @@ nodeunitShim({ const exportBucket = new s3.Bucket(stack, 'ExportBucket'); // THEN - test.throws(() => new DatabaseCluster(stack, 'Database', { + expect(() => new DatabaseCluster(stack, 'Database', { engine: DatabaseClusterEngine.AURORA, instances: 1, credentials: { @@ -1468,12 +1466,12 @@ nodeunitShim({ }, s3ExportRole: exportRole, s3ExportBuckets: [exportBucket], - })); + })).toThrow(); - test.done(); - }, - 'throws when s3ImportRole and s3ImportBuckets properties are both specified'(test: Test) { + }); + + test('throws when s3ImportRole and s3ImportBuckets properties are both specified', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1484,7 +1482,7 @@ nodeunitShim({ const importBucket = new s3.Bucket(stack, 'ImportBucket'); // THEN - test.throws(() => new DatabaseCluster(stack, 'Database', { + expect(() => new DatabaseCluster(stack, 'Database', { engine: DatabaseClusterEngine.AURORA, instances: 1, credentials: { @@ -1496,12 +1494,12 @@ nodeunitShim({ }, s3ImportRole: importRole, s3ImportBuckets: [importBucket], - })); + })).toThrow(); + - test.done(); - }, + }); - 'can set CloudWatch log exports'(test: Test) { + test('can set CloudWatch log exports', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1521,14 +1519,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::RDS::DBCluster', { + expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { EnableCloudwatchLogsExports: ['error', 'general', 'slowquery', 'audit'], - })); + }); + - test.done(); - }, + }); - 'can set CloudWatch log retention'(test: Test) { + test('can set CloudWatch log retention', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1549,7 +1547,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('Custom::LogRetention', { + expect(stack).toHaveResource('Custom::LogRetention', { ServiceToken: { 'Fn::GetAtt': [ 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A', @@ -1558,8 +1556,8 @@ nodeunitShim({ }, LogGroupName: { 'Fn::Join': ['', ['/aws/rds/cluster/', { Ref: 'DatabaseB269D8BB' }, '/error']] }, RetentionInDays: 90, - })); - expect(stack).to(haveResource('Custom::LogRetention', { + }); + expect(stack).toHaveResource('Custom::LogRetention', { ServiceToken: { 'Fn::GetAtt': [ 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A', @@ -1568,17 +1566,17 @@ nodeunitShim({ }, LogGroupName: { 'Fn::Join': ['', ['/aws/rds/cluster/', { Ref: 'DatabaseB269D8BB' }, '/general']] }, RetentionInDays: 90, - })); + }); - test.done(); - }, - 'throws if given unsupported CloudWatch log exports'(test: Test) { + }); + + test('throws if given unsupported CloudWatch log exports', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); - test.throws(() => { + expect(() => { new DatabaseCluster(stack, 'Database', { engine: DatabaseClusterEngine.AURORA, credentials: { @@ -1591,12 +1589,12 @@ nodeunitShim({ }, cloudwatchLogsExports: ['error', 'general', 'slowquery', 'audit', 'thislogdoesnotexist', 'neitherdoesthisone'], }); - }, /Unsupported logs for the current engine type: thislogdoesnotexist,neitherdoesthisone/); + }).toThrow(/Unsupported logs for the current engine type: thislogdoesnotexist,neitherdoesthisone/); + - test.done(); - }, + }); - 'can set deletion protection'(test: Test) { + test('can set deletion protection', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1616,14 +1614,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::RDS::DBCluster', { + expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { DeletionProtection: true, - })); + }); - test.done(); - }, - 'does not throw (but adds a node error) if a (dummy) VPC does not have sufficient subnets'(test: Test) { + }); + + test('does not throw (but adds a node error) if a (dummy) VPC does not have sufficient subnets', () => { // GIVEN const stack = testStack(); const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { isDefault: true }); @@ -1647,12 +1645,12 @@ nodeunitShim({ // THEN const art = SynthUtils.synthesize(stack); const meta = art.findMetadataByType('aws:cdk:error'); - test.equal(meta[0].data, 'Cluster requires at least 2 subnets, got 0'); + expect(meta[0].data).toEqual('Cluster requires at least 2 subnets, got 0'); + - test.done(); - }, + }); - 'create a cluster from a snapshot'(test: Test) { + test('create a cluster from a snapshot', () => { const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1666,7 +1664,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { Properties: { Engine: 'aurora', EngineVersion: '5.6.mysql_aurora.1.22.2', @@ -1676,14 +1674,14 @@ nodeunitShim({ }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - expect(stack).to(countResources('AWS::RDS::DBInstance', 2)); + expect(stack).toCountResources('AWS::RDS::DBInstance', 2); - test.done(); - }, - 'reuse an existing subnet group'(test: Test) { + }); + + test('reuse an existing subnet group', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1701,15 +1699,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::RDS::DBCluster', { + expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { DBSubnetGroupName: 'my-subnet-group', - })); - expect(stack).to(countResources('AWS::RDS::DBSubnetGroup', 0)); + }); + expect(stack).toCountResources('AWS::RDS::DBSubnetGroup', 0); + - test.done(); - }, + }); - 'defaultChild returns the DB Cluster'(test: Test) { + test('defaultChild returns the DB Cluster', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1726,12 +1724,12 @@ nodeunitShim({ }); // THEN - test.ok(cluster.node.defaultChild instanceof CfnDBCluster); + expect(cluster.node.defaultChild instanceof CfnDBCluster).toBeTruthy(); - test.done(); - }, - 'fromGeneratedSecret'(test: Test) { + }); + + test('fromGeneratedSecret', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1746,7 +1744,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBCluster', { + expect(stack).toHaveResource('AWS::RDS::DBCluster', { MasterUsername: 'admin', // username is a string MasterUserPassword: { 'Fn::Join': [ @@ -1760,12 +1758,12 @@ nodeunitShim({ ], ], }, - })); + }); - test.done(); - }, - 'can set public accessibility for database cluster with instances in private subnet'(test: Test) { + }); + + test('can set public accessibility for database cluster with instances in private subnet', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1782,15 +1780,15 @@ nodeunitShim({ }, }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { Engine: 'aurora', PubliclyAccessible: true, - })); + }); - test.done(); - }, - 'can set public accessibility for database cluster with instances in public subnet'(test: Test) { + }); + + test('can set public accessibility for database cluster with instances in public subnet', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1807,15 +1805,15 @@ nodeunitShim({ }, }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { Engine: 'aurora', PubliclyAccessible: false, - })); + }); - test.done(); - }, - 'database cluster instances in public subnet should by default have publiclyAccessible set to true'(test: Test) { + }); + + test('database cluster instances in public subnet should by default have publiclyAccessible set to true', () => { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -1831,13 +1829,47 @@ nodeunitShim({ }, }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { Engine: 'aurora', PubliclyAccessible: true, - })); + }); - test.done(); - }, + + }); +}); + +test.each([ + [cdk.RemovalPolicy.RETAIN, 'Retain', 'Retain', 'Retain'], + [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', 'Delete', ABSENT], + [cdk.RemovalPolicy.DESTROY, 'Delete', 'Delete', ABSENT], +])('if Cluster RemovalPolicy is \'%s\', the DBCluster has DeletionPolicy \'%s\', the DBInstance has \'%s\' and the DBSubnetGroup has \'%s\'', (clusterRemovalPolicy, clusterValue, instanceValue, subnetValue) => { + const stack = new cdk.Stack(); + + // WHEN + new DatabaseCluster(stack, 'Cluster', { + credentials: { username: 'admin' }, + engine: DatabaseClusterEngine.AURORA, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE), + vpc: new ec2.Vpc(stack, 'Vpc'), + }, + removalPolicy: clusterRemovalPolicy, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { + DeletionPolicy: clusterValue, + UpdateReplacePolicy: clusterValue, + }, ResourcePart.CompleteDefinition); + + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { + DeletionPolicy: instanceValue, + UpdateReplacePolicy: instanceValue, + }, ResourcePart.CompleteDefinition); + + expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { + DeletionPolicy: subnetValue, + }, ResourcePart.CompleteDefinition); }); test.each([ @@ -1859,25 +1891,25 @@ test.each([ }); // THEN - expect(stack).to(haveResourceLike('AWS::RDS::DBCluster', { + expect(stack).toHaveResourceLike('AWS::RDS::DBCluster', { DeletionPolicy: clusterValue, UpdateReplacePolicy: clusterValue, - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { DeletionPolicy: instanceValue, UpdateReplacePolicy: instanceValue, - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - expect(stack).to(haveResourceLike('AWS::RDS::DBSubnetGroup', { + expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { DeletionPolicy: subnetValue, UpdateReplacePolicy: subnetValue, - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); }); -function testStack() { - const stack = new cdk.Stack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); +function testStack(app?: cdk.App) { + const stack = new cdk.Stack(app, undefined, { env: { account: '12345', region: 'us-test-1' } }); stack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); return stack; } diff --git a/packages/@aws-cdk/aws-rds/test/instance.test.ts b/packages/@aws-cdk/aws-rds/test/instance.test.ts index 625dfa7074ae3..497bfbea36c32 100644 --- a/packages/@aws-cdk/aws-rds/test/instance.test.ts +++ b/packages/@aws-cdk/aws-rds/test/instance.test.ts @@ -1,4 +1,5 @@ -import { ABSENT, countResources, expect, haveResource, ResourcePart, haveResourceLike, anything } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import { ABSENT, ResourcePart, anything } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as targets from '@aws-cdk/aws-events-targets'; import { ManagedPolicy, Role, ServicePrincipal, AccountPrincipal } from '@aws-cdk/aws-iam'; @@ -7,20 +8,20 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; +import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as rds from '../lib'; let stack: cdk.Stack; let vpc: ec2.Vpc; -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('instance', () => { + beforeEach(() => { stack = new cdk.Stack(); vpc = new ec2.Vpc(stack, 'VPC'); - cb(); - }, + }); - 'create a DB instance'(test: Test) { + test('create a DB instance', () => { // WHEN new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.oracleSe2({ version: rds.OracleEngineVersion.VER_19_0_0_0_2020_04_R1 }), @@ -48,7 +49,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { Properties: { DBInstanceClass: 'db.t2.medium', AllocatedStorage: '100', @@ -116,9 +117,9 @@ nodeunitShim({ }, DeletionPolicy: 'Snapshot', UpdateReplacePolicy: 'Snapshot', - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - expect(stack).to(haveResource('AWS::RDS::DBSubnetGroup', { + expect(stack).toHaveResource('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'Subnet group for Instance database', SubnetIds: [ { @@ -128,13 +129,13 @@ nodeunitShim({ Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', }, ], - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Security group for Instance database', - })); + }); - expect(stack).to(haveResource('AWS::IAM::Role', { + expect(stack).toHaveResource('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -161,9 +162,9 @@ nodeunitShim({ ], }, ], - })); + }); - expect(stack).to(haveResource('AWS::SecretsManager::Secret', { + expect(stack).toHaveResource('AWS::SecretsManager::Secret', { Description: { 'Fn::Join': [ '', @@ -181,9 +182,9 @@ nodeunitShim({ PasswordLength: 30, SecretStringTemplate: '{"username":"syscdk"}', }, - })); + }); - expect(stack).to(haveResource('AWS::SecretsManager::SecretTargetAttachment', { + expect(stack).toHaveResource('AWS::SecretsManager::SecretTargetAttachment', { SecretId: { Ref: 'InstanceSecret478E0A47', }, @@ -191,14 +192,14 @@ nodeunitShim({ Ref: 'InstanceC1063A87', }, TargetType: 'AWS::RDS::DBInstance', - })); + }); + + expect(stack).toCountResources('Custom::LogRetention', 4); - expect(stack).to(countResources('Custom::LogRetention', 4)); - test.done(); - }, + }); - 'instance with option and parameter group'(test: Test) { + test('instance with option and parameter group', () => { const optionGroup = new rds.OptionGroup(stack, 'OptionGroup', { engine: rds.DatabaseInstanceEngine.oracleSe2({ version: rds.OracleEngineVersion.VER_19_0_0_0_2020_04_R1 }), configurations: [ @@ -227,19 +228,19 @@ nodeunitShim({ parameterGroup, }); - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { DBParameterGroupName: { Ref: 'ParameterGroup5E32DECB', }, OptionGroupName: { Ref: 'OptionGroupACA43DC1', }, - })); + }); - test.done(); - }, - 'can specify subnet type'(test: Test) { + }); + + test('can specify subnet type', () => { new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19, @@ -251,13 +252,13 @@ nodeunitShim({ }, }); - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { DBSubnetGroupName: { Ref: 'InstanceSubnetGroupF2CBA54F', }, PubliclyAccessible: false, - })); - expect(stack).to(haveResource('AWS::RDS::DBSubnetGroup', { + }); + expect(stack).toHaveResource('AWS::RDS::DBSubnetGroup', { DBSubnetGroupDescription: 'Subnet group for Instance database', SubnetIds: [ { @@ -267,13 +268,13 @@ nodeunitShim({ Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', }, ], - })); + }); - test.done(); - }, - 'DatabaseInstanceFromSnapshot': { - 'create an instance from snapshot'(test: Test) { + }); + + describe('DatabaseInstanceFromSnapshot', () => { + test('create an instance from snapshot', () => { new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }), @@ -281,14 +282,14 @@ nodeunitShim({ vpc, }); - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { DBSnapshotIdentifier: 'my-snapshot', - })); + }); + - test.done(); - }, + }); - 'can generate a new snapshot password'(test: Test) { + test('can generate a new snapshot password', () => { new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), @@ -298,13 +299,13 @@ nodeunitShim({ }), }); - expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { MasterUsername: ABSENT, MasterUserPassword: { 'Fn::Join': ['', ['{{resolve:secretsmanager:', { Ref: 'InstanceSecret478E0A47' }, ':SecretString:password::}}']], }, - })); - expect(stack).to(haveResource('AWS::SecretsManager::Secret', { + }); + expect(stack).toHaveResource('AWS::SecretsManager::Secret', { Description: { 'Fn::Join': ['', ['Generated by the CDK for stack: ', { Ref: 'AWS::StackName' }]], }, @@ -314,12 +315,12 @@ nodeunitShim({ PasswordLength: 30, SecretStringTemplate: '{"username":"admin"}', }, - })); + }); - test.done(); - }, - 'fromGeneratedSecret'(test: Test) { + }); + + test('fromGeneratedSecret', () => { new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), @@ -329,29 +330,29 @@ nodeunitShim({ }), }); - expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { MasterUsername: ABSENT, MasterUserPassword: { // logical id of secret has a hash 'Fn::Join': ['', ['{{resolve:secretsmanager:', { Ref: 'InstanceSecretB6DFA6BE8ee0a797cad8a68dbeb85f8698cdb5bb' }, ':SecretString:password::}}']], }, - })); + }); + - test.done(); - }, + }); - 'throws if generating a new password without a username'(test: Test) { - test.throws(() => new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { + test('throws if generating a new password without a username', () => { + expect(() => new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), vpc, credentials: { generatePassword: true }, - }), /`credentials` `username` must be specified when `generatePassword` is set to true/); + })).toThrow(/`credentials` `username` must be specified when `generatePassword` is set to true/); - test.done(); - }, - 'can set a new snapshot password from an existing SecretValue'(test: Test) { + }); + + test('can set a new snapshot password from an existing SecretValue', () => { new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), @@ -360,15 +361,15 @@ nodeunitShim({ }); // TODO - Expect this to be broken - expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { MasterUsername: ABSENT, MasterUserPassword: 'mysecretpassword', - })); + }); - test.done(); - }, - 'can set a new snapshot password from an existing Secret'(test: Test) { + }); + + test('can set a new snapshot password from an existing Secret', () => { const secret = new rds.DatabaseSecret(stack, 'DBSecret', { username: 'admin', encryptionKey: new kms.Key(stack, 'PasswordKey'), @@ -380,18 +381,18 @@ nodeunitShim({ credentials: rds.SnapshotCredentials.fromSecret(secret), }); - expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { MasterUsername: ABSENT, MasterUserPassword: { 'Fn::Join': ['', ['{{resolve:secretsmanager:', { Ref: 'DBSecretD58955BC' }, ':SecretString:password::}}']], }, - })); + }); + - test.done(); - }, - }, + }); + }); - 'create a read replica in the same region - with the subnet group name'(test: Test) { + test('create a read replica in the same region - with the subnet group name', () => { const sourceInstance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), @@ -406,7 +407,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { SourceDBInstanceIdentifier: { 'Fn::Join': ['', [ 'arn:', @@ -422,12 +423,12 @@ nodeunitShim({ DBSubnetGroupName: { Ref: 'ReadReplicaSubnetGroup680C605C', }, - })); + }); + - test.done(); - }, + }); - 'on event'(test: Test) { + test('on event', () => { const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, vpc, @@ -442,7 +443,7 @@ nodeunitShim({ instance.onEvent('InstanceEvent', { target: new targets.LambdaFunction(fn) }); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { EventPattern: { source: [ 'aws.rds', @@ -484,12 +485,12 @@ nodeunitShim({ Id: 'Target0', }, ], - })); + }); - test.done(); - }, - 'on event without target'(test: Test) { + }); + + test('on event without target', () => { const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, vpc, @@ -499,7 +500,7 @@ nodeunitShim({ instance.onEvent('InstanceEvent'); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { EventPattern: { source: [ 'aws.rds', @@ -530,12 +531,12 @@ nodeunitShim({ }, ], }, - })); + }); - test.done(); - }, - 'can use metricCPUUtilization'(test: Test) { + }); + + test('can use metricCPUUtilization', () => { // WHEN const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, @@ -543,7 +544,7 @@ nodeunitShim({ }); // THEN - test.deepEqual(stack.resolve(instance.metricCPUUtilization()), { + expect(stack.resolve(instance.metricCPUUtilization())).toEqual({ dimensions: { DBInstanceIdentifier: { Ref: 'InstanceC1063A87' } }, namespace: 'AWS/RDS', metricName: 'CPUUtilization', @@ -551,21 +552,21 @@ nodeunitShim({ statistic: 'Average', }); - test.done(); - }, - 'can resolve endpoint port and socket address'(test: Test) { + }); + + test('can resolve endpoint port and socket address', () => { // WHEN const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, vpc, }); - test.deepEqual(stack.resolve(instance.instanceEndpoint.port), { + expect(stack.resolve(instance.instanceEndpoint.port)).toEqual({ 'Fn::GetAtt': ['InstanceC1063A87', 'Endpoint.Port'], }); - test.deepEqual(stack.resolve(instance.instanceEndpoint.socketAddress), { + expect(stack.resolve(instance.instanceEndpoint.socketAddress)).toEqual({ 'Fn::Join': [ '', [ @@ -576,10 +577,10 @@ nodeunitShim({ ], }); - test.done(); - }, - 'can deactivate backup'(test: Test) { + }); + + test('can deactivate backup', () => { // WHEN new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, @@ -588,14 +589,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { BackupRetentionPeriod: 0, - })); + }); + - test.done(); - }, + }); - 'imported instance with imported security group with allowAllOutbound set to false'(test: Test) { + test('imported instance with imported security group with allowAllOutbound set to false', () => { const instance = rds.DatabaseInstance.fromDatabaseInstanceAttributes(stack, 'Database', { instanceEndpointAddress: 'address', instanceIdentifier: 'identifier', @@ -609,14 +610,14 @@ nodeunitShim({ instance.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-123456789', - })); + }); - test.done(); - }, - 'create an instance with imported monitoring role'(test: Test) { + }); + + test('create an instance with imported monitoring role', () => { const monitoringRole = new Role(stack, 'MonitoringRole', { assumedBy: new ServicePrincipal('monitoring.rds.amazonaws.com'), managedPolicies: [ @@ -633,17 +634,17 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { MonitoringInterval: 60, MonitoringRoleArn: { 'Fn::GetAtt': ['MonitoringRole90457BF9', 'Arn'], }, - }, ResourcePart.Properties)); + }, ResourcePart.Properties); - test.done(); - }, - 'create an instance with an existing security group'(test: Test) { + }); + + test('create an instance with an existing security group', () => { const securityGroup = ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { allowAllOutbound: false, }); @@ -657,11 +658,11 @@ nodeunitShim({ instance.connections.allowDefaultPortFromAnyIpv4(); // THEN - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { VPCSecurityGroups: ['sg-123456789'], - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { FromPort: { 'Fn::GetAtt': [ 'InstanceC1063A87', @@ -675,12 +676,12 @@ nodeunitShim({ 'Endpoint.Port', ], }, - })); + }); - test.done(); - }, - 'throws when trying to add rotation to an instance without secret'(test: Test) { + }); + + test('throws when trying to add rotation to an instance without secret', () => { const instance = new rds.DatabaseInstance(stack, 'Database', { engine: rds.DatabaseInstanceEngine.SQL_SERVER_EE, credentials: rds.Credentials.fromUsername('syscdk', { password: cdk.SecretValue.plainText('tooshort') }), @@ -688,12 +689,12 @@ nodeunitShim({ }); // THEN - test.throws(() => instance.addRotationSingleUser(), /without secret/); + expect(() => instance.addRotationSingleUser()).toThrow(/without secret/); - test.done(); - }, - 'throws when trying to add single user rotation multiple times'(test: Test) { + }); + + test('throws when trying to add single user rotation multiple times', () => { const instance = new rds.DatabaseInstance(stack, 'Database', { engine: rds.DatabaseInstanceEngine.SQL_SERVER_EE, instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), @@ -705,12 +706,12 @@ nodeunitShim({ instance.addRotationSingleUser(); // THEN - test.throws(() => instance.addRotationSingleUser(), /A single user rotation was already added to this instance/); + expect(() => instance.addRotationSingleUser()).toThrow(/A single user rotation was already added to this instance/); - test.done(); - }, - 'throws when timezone is set for non-sqlserver database engine'(test: Test) { + }); + + test('throws when timezone is set for non-sqlserver database engine', () => { const tzSupportedEngines = [rds.DatabaseInstanceEngine.SQL_SERVER_EE, rds.DatabaseInstanceEngine.SQL_SERVER_EX, rds.DatabaseInstanceEngine.SQL_SERVER_SE, rds.DatabaseInstanceEngine.SQL_SERVER_WEB]; const tzUnsupportedEngines = [rds.DatabaseInstanceEngine.MYSQL, rds.DatabaseInstanceEngine.POSTGRES, @@ -718,25 +719,25 @@ nodeunitShim({ // THEN tzSupportedEngines.forEach((engine) => { - test.ok(new rds.DatabaseInstance(stack, `${engine.engineType}-db`, { + expect(new rds.DatabaseInstance(stack, `${engine.engineType}-db`, { engine, timezone: 'Europe/Zurich', vpc, - })); + })).toBeDefined(); }); tzUnsupportedEngines.forEach((engine) => { - test.throws(() => new rds.DatabaseInstance(stack, `${engine.engineType}-db`, { + expect(() => new rds.DatabaseInstance(stack, `${engine.engineType}-db`, { engine, timezone: 'Europe/Zurich', vpc, - }), /timezone property can not be configured for/); + })).toThrow(/timezone property can not be configured for/); }); - test.done(); - }, - 'create an instance from snapshot with maximum allocated storage'(test: Test) { + }); + + test('create an instance from snapshot with maximum allocated storage', () => { // WHEN new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', @@ -746,15 +747,15 @@ nodeunitShim({ maxAllocatedStorage: 200, }); - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { DBSnapshotIdentifier: 'my-snapshot', MaxAllocatedStorage: 200, - })); + }); + - test.done(); - }, + }); - 'create a DB instance with maximum allocated storage'(test: Test) { + test('create a DB instance with maximum allocated storage', () => { // WHEN new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.MYSQL, @@ -764,28 +765,28 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { BackupRetentionPeriod: 0, MaxAllocatedStorage: 250, - })); + }); - test.done(); - }, - 'iam authentication - off by default'(test: Test) { + }); + + test('iam authentication - off by default', () => { new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), vpc, }); - expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { EnableIAMDatabaseAuthentication: ABSENT, - })); + }); - test.done(); - }, - 'createGrant - creates IAM policy and enables IAM auth'(test: Test) { + }); + + test('createGrant - creates IAM policy and enables IAM auth', () => { const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), vpc, @@ -795,10 +796,10 @@ nodeunitShim({ }); instance.grantConnect(role); - expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { EnableIAMDatabaseAuthentication: true, - })); - expect(stack).to(haveResource('AWS::IAM::Policy', { + }); + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Effect: 'Allow', @@ -809,12 +810,12 @@ nodeunitShim({ }], Version: '2012-10-17', }, - })); + }); - test.done(); - }, - 'createGrant - throws if IAM auth disabled'(test: Test) { + }); + + test('createGrant - throws if IAM auth disabled', () => { const instance = new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), vpc, @@ -824,12 +825,12 @@ nodeunitShim({ assumedBy: new AccountPrincipal(stack.account), }); - test.throws(() => { instance.grantConnect(role); }, /Cannot grant connect when IAM authentication is disabled/); + expect(() => { instance.grantConnect(role); }).toThrow(/Cannot grant connect when IAM authentication is disabled/); - test.done(); - }, - 'domain - sets domain property'(test: Test) { + }); + + test('domain - sets domain property', () => { const domain = 'd-90670a8d36'; // WHEN @@ -840,14 +841,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { Domain: domain, - })); + }); + - test.done(); - }, + }); - 'domain - uses role if provided'(test: Test) { + test('domain - uses role if provided', () => { const domain = 'd-90670a8d36'; // WHEN @@ -860,15 +861,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { Domain: domain, DomainIAMRoleName: stack.resolve(role.roleName), - })); + }); - test.done(); - }, - 'domain - creates role if not provided'(test: Test) { + }); + + test('domain - creates role if not provided', () => { const domain = 'd-90670a8d36'; // WHEN @@ -879,12 +880,12 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { Domain: domain, DomainIAMRoleName: anything(), - })); + }); - expect(stack).to(haveResource('AWS::IAM::Role', { + expect(stack).toHaveResource('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -911,12 +912,12 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'throws when domain is set for mariadb database engine'(test: Test) { + }); + + test('throws when domain is set for mariadb database engine', () => { const domainSupportedEngines = [rds.DatabaseInstanceEngine.SQL_SERVER_EE, rds.DatabaseInstanceEngine.SQL_SERVER_EX, rds.DatabaseInstanceEngine.SQL_SERVER_SE, rds.DatabaseInstanceEngine.SQL_SERVER_WEB, rds.DatabaseInstanceEngine.MYSQL, rds.DatabaseInstanceEngine.POSTGRES, rds.DatabaseInstanceEngine.ORACLE_EE]; @@ -924,28 +925,28 @@ nodeunitShim({ // THEN domainSupportedEngines.forEach((engine) => { - test.ok(new rds.DatabaseInstance(stack, `${engine.engineType}-db`, { + expect(() => new rds.DatabaseInstance(stack, `${engine.engineType}-db`, { engine, domain: 'd-90670a8d36', vpc, - })); + })).not.toThrow(); }); domainUnsupportedEngines.forEach((engine) => { const expectedError = new RegExp(`domain property cannot be configured for ${engine.engineType}`); - test.throws(() => new rds.DatabaseInstance(stack, `${engine.engineType}-db`, { + expect(() => new rds.DatabaseInstance(stack, `${engine.engineType}-db`, { engine, domain: 'd-90670a8d36', vpc, - }), expectedError); + })).toThrow(expectedError); }); - test.done(); - }, - 'performance insights': { - 'instance with all performance insights properties'(test: Test) { + }); + + describe('performance insights', () => { + test('instance with all performance insights properties', () => { new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), vpc, @@ -954,72 +955,72 @@ nodeunitShim({ performanceInsightEncryptionKey: new kms.Key(stack, 'Key'), }); - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { EnablePerformanceInsights: true, PerformanceInsightsRetentionPeriod: 731, PerformanceInsightsKMSKeyId: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, - })); + }); - test.done(); - }, - 'setting performance insights fields enables performance insights'(test: Test) { + }); + + test('setting performance insights fields enables performance insights', () => { new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), vpc, performanceInsightRetention: rds.PerformanceInsightRetention.LONG_TERM, }); - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { EnablePerformanceInsights: true, PerformanceInsightsRetentionPeriod: 731, - })); + }); + - test.done(); - }, + }); - 'throws if performance insights fields are set but performance insights is disabled'(test: Test) { - test.throws(() => { + test('throws if performance insights fields are set but performance insights is disabled', () => { + expect(() => { new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), vpc, enablePerformanceInsights: false, performanceInsightRetention: rds.PerformanceInsightRetention.DEFAULT, }); - }, /`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set/); + }).toThrow(/`enablePerformanceInsights` disabled, but `performanceInsightRetention` or `performanceInsightEncryptionKey` was set/); + - test.done(); - }, - }, + }); + }); - 'reuse an existing subnet group'(test: Test) { + test('reuse an existing subnet group', () => { new rds.DatabaseInstance(stack, 'Database', { engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }), vpc, subnetGroup: rds.SubnetGroup.fromSubnetGroupName(stack, 'SubnetGroup', 'my-subnet-group'), }); - expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { DBSubnetGroupName: 'my-subnet-group', - })); - expect(stack).to(countResources('AWS::RDS::DBSubnetGroup', 0)); + }); + expect(stack).toCountResources('AWS::RDS::DBSubnetGroup', 0); + - test.done(); - }, + }); - 'defaultChild returns the DB Instance'(test: Test) { + test('defaultChild returns the DB Instance', () => { const instance = new rds.DatabaseInstance(stack, 'Database', { engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }), vpc, }); // THEN - test.ok(instance.node.defaultChild instanceof rds.CfnDBInstance); + expect(instance.node.defaultChild instanceof rds.CfnDBInstance).toBeTruthy(); - test.done(); - }, - "PostgreSQL database instance uses a different default master username than 'admin', which is a reserved word"(test: Test) { + }); + + test("PostgreSQL database instance uses a different default master username than 'admin', which is a reserved word", () => { new rds.DatabaseInstance(stack, 'Instance', { vpc, engine: rds.DatabaseInstanceEngine.postgres({ @@ -1028,17 +1029,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::SecretsManager::Secret', { + expect(stack).toHaveResourceLike('AWS::SecretsManager::Secret', { GenerateSecretString: { SecretStringTemplate: '{"username":"postgres"}', }, - })); + }); + - test.done(); - }, + }); - 'S3 Import/Export': { - 'instance with s3 import and export buckets'(test: Test) { + describe('S3 Import/Export', () => { + testFutureBehavior('instance with s3 import and export buckets', { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }, cdk.App, (app) => { + stack = new cdk.Stack(app); + vpc = new ec2.Vpc(stack, 'VPC'); new rds.DatabaseInstance(stack, 'DB', { engine: rds.DatabaseInstanceEngine.sqlServerSe({ version: rds.SqlServerEngineVersion.VER_14_00_3192_2_V1 }), vpc, @@ -1046,7 +1049,7 @@ nodeunitShim({ s3ExportBuckets: [new s3.Bucket(stack, 'S3Export')], }); - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { AssociatedRoles: [ { FeatureName: 'S3_INTEGRATION', @@ -1054,10 +1057,10 @@ nodeunitShim({ }, ], OptionGroupName: { Ref: 'DBInstanceOptionGroup46C68006' }, - })); + }); // Can read from import bucket, and read/write from export bucket - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ Action: [ @@ -1077,7 +1080,7 @@ nodeunitShim({ 's3:GetBucket*', 's3:List*', 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], Effect: 'Allow', @@ -1088,58 +1091,58 @@ nodeunitShim({ }], Version: '2012-10-17', }, - })); + }); + - test.done(); - }, + }); - 'throws if using s3 import on unsupported engine'(test: Test) { + test('throws if using s3 import on unsupported engine', () => { const s3ImportRole = new Role(stack, 'S3ImportRole', { assumedBy: new ServicePrincipal('rds.amazonaws.com'), }); - test.throws(() => { + expect(() => { new rds.DatabaseInstance(stack, 'DBWithImportBucket', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), vpc, s3ImportBuckets: [new s3.Bucket(stack, 'S3Import')], }); - }, /Engine 'mysql-8.0.19' does not support S3 import/); - test.throws(() => { + }).toThrow(/Engine 'mysql-8.0.19' does not support S3 import/); + expect(() => { new rds.DatabaseInstance(stack, 'DBWithImportRole', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), vpc, s3ImportRole, }); - }, /Engine 'mysql-8.0.19' does not support S3 import/); + }).toThrow(/Engine 'mysql-8.0.19' does not support S3 import/); + - test.done(); - }, + }); - 'throws if using s3 export on unsupported engine'(test: Test) { + test('throws if using s3 export on unsupported engine', () => { const s3ExportRole = new Role(stack, 'S3ExportRole', { assumedBy: new ServicePrincipal('rds.amazonaws.com'), }); - test.throws(() => { + expect(() => { new rds.DatabaseInstance(stack, 'DBWithExportBucket', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), vpc, s3ExportBuckets: [new s3.Bucket(stack, 'S3Export')], }); - }, /Engine 'mysql-8.0.19' does not support S3 export/); - test.throws(() => { + }).toThrow(/Engine 'mysql-8.0.19' does not support S3 export/); + expect(() => { new rds.DatabaseInstance(stack, 'DBWithExportRole', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), vpc, s3ExportRole: s3ExportRole, }); - }, /Engine 'mysql-8.0.19' does not support S3 export/); + }).toThrow(/Engine 'mysql-8.0.19' does not support S3 export/); + - test.done(); - }, + }); - 'throws if provided two different roles for import/export'(test: Test) { + test('throws if provided two different roles for import/export', () => { const s3ImportRole = new Role(stack, 'S3ImportRole', { assumedBy: new ServicePrincipal('rds.amazonaws.com'), }); @@ -1147,20 +1150,20 @@ nodeunitShim({ assumedBy: new ServicePrincipal('rds.amazonaws.com'), }); - test.throws(() => { + expect(() => { new rds.DatabaseInstance(stack, 'DBWithExportBucket', { engine: rds.DatabaseInstanceEngine.sqlServerEe({ version: rds.SqlServerEngineVersion.VER_14_00_3192_2_V1 }), vpc, s3ImportRole, s3ExportRole, }); - }, /S3 import and export roles must be the same/); + }).toThrow(/S3 import and export roles must be the same/); - test.done(); - }, - }, - 'fromGeneratedSecret'(test: Test) { + }); + }); + + test('fromGeneratedSecret', () => { // WHEN new rds.DatabaseInstance(stack, 'Database', { engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }), @@ -1169,7 +1172,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { MasterUsername: 'postgres', // username is a string MasterUserPassword: { 'Fn::Join': [ @@ -1183,12 +1186,12 @@ nodeunitShim({ ], ], }, - })); + }); + - test.done(); - }, + }); - 'fromPassword'(test: Test) { + test('fromPassword', () => { // WHEN new rds.DatabaseInstance(stack, 'Database', { engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }), @@ -1197,15 +1200,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { MasterUsername: 'postgres', // username is a string MasterUserPassword: '{{resolve:ssm-secure:/dbPassword:1}}', // reference to SSM - })); + }); - test.done(); - }, - 'can set publiclyAccessible to false with public subnets'(test: Test) { + }); + + test('can set publiclyAccessible to false with public subnets', () => { new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19, @@ -1217,14 +1220,14 @@ nodeunitShim({ publiclyAccessible: false, }); - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { PubliclyAccessible: false, - })); + }); + - test.done(); - }, + }); - 'can set publiclyAccessible to true with private subnets'(test: Test) { + test('can set publiclyAccessible to true with private subnets', () => { new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19, @@ -1236,12 +1239,10 @@ nodeunitShim({ publiclyAccessible: true, }); - expect(stack).to(haveResource('AWS::RDS::DBInstance', { + expect(stack).toHaveResource('AWS::RDS::DBInstance', { PubliclyAccessible: true, - })); - - test.done(); - }, + }); + }); }); test.each([ @@ -1249,6 +1250,10 @@ test.each([ [cdk.RemovalPolicy.SNAPSHOT, 'Snapshot', ABSENT], [cdk.RemovalPolicy.DESTROY, 'Delete', ABSENT], ])('if Instance RemovalPolicy is \'%s\', the instance has DeletionPolicy \'%s\' and the DBSubnetGroup has \'%s\'', (instanceRemovalPolicy, instanceValue, subnetValue) => { + // GIVEN + stack = new cdk.Stack(); + vpc = new ec2.Vpc(stack, 'VPC'); + // WHEN new rds.DatabaseInstance(stack, 'Instance', { engine: rds.DatabaseInstanceEngine.mysql({ @@ -1260,13 +1265,13 @@ test.each([ }); // THEN - expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + expect(stack).toHaveResourceLike('AWS::RDS::DBInstance', { DeletionPolicy: instanceValue, UpdateReplacePolicy: instanceValue, - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - expect(stack).to(haveResourceLike('AWS::RDS::DBSubnetGroup', { + expect(stack).toHaveResourceLike('AWS::RDS::DBSubnetGroup', { DeletionPolicy: subnetValue, UpdateReplacePolicy: subnetValue, - }, ResourcePart.CompleteDefinition)); -}); + }, ResourcePart.CompleteDefinition); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json index 8a05969830f29..0de11e4fd5c4f 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json @@ -909,7 +909,7 @@ ] } }, - "InstanceAvailabilityAllowEventRuleawscdkrdsinstanceInstanceAvailabilityCE39A6A7B066AA0D": { + "InstanceAvailabilityAllowEventRuleawscdkrdsinstanceFunctionD515EE1969208105": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", 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-route53/lib/record-set.ts b/packages/@aws-cdk/aws-route53/lib/record-set.ts index 577af5c1a3a57..a86195f1d0055 100644 --- a/packages/@aws-cdk/aws-route53/lib/record-set.ts +++ b/packages/@aws-cdk/aws-route53/lib/record-set.ts @@ -604,7 +604,7 @@ export class CrossAccountZoneDelegationRecord extends CoreConstruct { const serviceToken = CustomResourceProvider.getOrCreate(this, CROSS_ACCOUNT_ZONE_DELEGATION_RESOURCE_TYPE, { codeDirectory: path.join(__dirname, 'cross-account-zone-delegation-handler'), - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, policyStatements: [{ Effect: 'Allow', Action: 'sts:AssumeRole', Resource: props.delegationRole.roleArn }], }); diff --git a/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json b/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json index df7ab45491d39..5c4f8ee9629df 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json +++ b/packages/@aws-cdk/aws-route53/test/integ.vpc-endpoint-service-domain-name.expected.json @@ -606,40 +606,40 @@ ] }, "Create": { - "service": "EC2", - "action": "modifyVpcEndpointServiceConfiguration", - "parameters": { - "ServiceId": { - "Ref": "VPCES3AE7D565" - }, - "PrivateDnsName": "my-stuff.aws-cdk.dev" - }, - "physicalResourceId": { - "id": "awscdkvpcendpointdnsintegVPCES2D7BC258" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"EC2\",\"action\":\"modifyVpcEndpointServiceConfiguration\",\"parameters\":{\"ServiceId\":\"", + { + "Ref": "VPCES3AE7D565" + }, + "\",\"PrivateDnsName\":\"my-stuff.aws-cdk.dev\"},\"physicalResourceId\":{\"id\":\"awscdkvpcendpointdnsintegVPCES2D7BC258\"}}" + ] + ] }, "Update": { - "service": "EC2", - "action": "modifyVpcEndpointServiceConfiguration", - "parameters": { - "ServiceId": { - "Ref": "VPCES3AE7D565" - }, - "PrivateDnsName": "my-stuff.aws-cdk.dev" - }, - "physicalResourceId": { - "id": "awscdkvpcendpointdnsintegVPCES2D7BC258" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"EC2\",\"action\":\"modifyVpcEndpointServiceConfiguration\",\"parameters\":{\"ServiceId\":\"", + { + "Ref": "VPCES3AE7D565" + }, + "\",\"PrivateDnsName\":\"my-stuff.aws-cdk.dev\"},\"physicalResourceId\":{\"id\":\"awscdkvpcendpointdnsintegVPCES2D7BC258\"}}" + ] + ] }, "Delete": { - "service": "EC2", - "action": "modifyVpcEndpointServiceConfiguration", - "parameters": { - "ServiceId": { - "Ref": "VPCES3AE7D565" - }, - "RemovePrivateDnsName": "TRUE:BOOLEAN" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"EC2\",\"action\":\"modifyVpcEndpointServiceConfiguration\",\"parameters\":{\"ServiceId\":\"", + { + "Ref": "VPCES3AE7D565" + }, + "\",\"RemovePrivateDnsName\":true}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -686,32 +686,28 @@ ] }, "Create": { - "service": "EC2", - "action": "describeVpcEndpointServiceConfigurations", - "parameters": { - "ServiceIds": [ + "Fn::Join": [ + "", + [ + "{\"service\":\"EC2\",\"action\":\"describeVpcEndpointServiceConfigurations\",\"parameters\":{\"ServiceIds\":[\"", { "Ref": "VPCES3AE7D565" - } + }, + "\"]},\"physicalResourceId\":{\"id\":\"0b26ca4969ad06c279e229b1b55b9bc2\"}}" ] - }, - "physicalResourceId": { - "id": "0b26ca4969ad06c279e229b1b55b9bc2" - } + ] }, "Update": { - "service": "EC2", - "action": "describeVpcEndpointServiceConfigurations", - "parameters": { - "ServiceIds": [ + "Fn::Join": [ + "", + [ + "{\"service\":\"EC2\",\"action\":\"describeVpcEndpointServiceConfigurations\",\"parameters\":{\"ServiceIds\":[\"", { "Ref": "VPCES3AE7D565" - } + }, + "\"]},\"physicalResourceId\":{\"id\":\"0b26ca4969ad06c279e229b1b55b9bc2\"}}" ] - }, - "physicalResourceId": { - "id": "0b26ca4969ad06c279e229b1b55b9bc2" - } + ] }, "InstallLatestAwsSdk": true }, @@ -831,64 +827,68 @@ ] }, "Create": { - "service": "EC2", - "action": "startVpcEndpointServicePrivateDnsVerification", - "parameters": { - "ServiceId": { - "Ref": "VPCES3AE7D565" - } - }, - "physicalResourceId": { - "id": { - "Fn::Join": [ - ":", - [ - { - "Fn::GetAtt": [ - "EndpointDomainGetNames9E697ED2", - "ServiceConfigurations.0.PrivateDnsNameConfiguration.Name" - ] - }, - { - "Fn::GetAtt": [ - "EndpointDomainGetNames9E697ED2", - "ServiceConfigurations.0.PrivateDnsNameConfiguration.Value" - ] - } + "Fn::Join": [ + "", + [ + "{\"service\":\"EC2\",\"action\":\"startVpcEndpointServicePrivateDnsVerification\",\"parameters\":{\"ServiceId\":\"", + { + "Ref": "VPCES3AE7D565" + }, + "\"},\"physicalResourceId\":{\"id\":\"", + { + "Fn::Join": [ + ":", + [ + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Name" + ] + }, + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Value" + ] + } + ] ] - ] - } - } + }, + "\"}}" + ] + ] }, "Update": { - "service": "EC2", - "action": "startVpcEndpointServicePrivateDnsVerification", - "parameters": { - "ServiceId": { - "Ref": "VPCES3AE7D565" - } - }, - "physicalResourceId": { - "id": { - "Fn::Join": [ - ":", - [ - { - "Fn::GetAtt": [ - "EndpointDomainGetNames9E697ED2", - "ServiceConfigurations.0.PrivateDnsNameConfiguration.Name" - ] - }, - { - "Fn::GetAtt": [ - "EndpointDomainGetNames9E697ED2", - "ServiceConfigurations.0.PrivateDnsNameConfiguration.Value" - ] - } + "Fn::Join": [ + "", + [ + "{\"service\":\"EC2\",\"action\":\"startVpcEndpointServicePrivateDnsVerification\",\"parameters\":{\"ServiceId\":\"", + { + "Ref": "VPCES3AE7D565" + }, + "\"},\"physicalResourceId\":{\"id\":\"", + { + "Fn::Join": [ + ":", + [ + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Name" + ] + }, + { + "Fn::GetAtt": [ + "EndpointDomainGetNames9E697ED2", + "ServiceConfigurations.0.PrivateDnsNameConfiguration.Value" + ] + } + ] ] - ] - } - } + }, + "\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, @@ -936,7 +936,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904" }, "S3Key": { "Fn::Join": [ @@ -949,7 +949,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -962,7 +962,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0" + "Ref": "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF" } ] } @@ -972,13 +972,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs12.x", "Timeout": 120 }, @@ -988,17 +988,17 @@ } }, "Parameters": { - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3Bucket38F1BB8E": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3Bucket65227904": { "Type": "String", - "Description": "S3 bucket for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 bucket for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94S3VersionKeyCCDC67C0": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343S3VersionKey3AF0E7DF": { "Type": "String", - "Description": "S3 key for asset version \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "S3 key for asset version \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" }, - "AssetParametersb64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94ArtifactHash782948FC": { + "AssetParameters0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343ArtifactHash0C561FF5": { "Type": "String", - "Description": "Artifact hash for asset \"b64b129569a5ac7a9abf88a18ac0b504d1fb1208872460476ed3fd435830eb94\"" + "Description": "Artifact hash for asset \"0625b1566df06e0ffd948f0f65f97a3d22d48242e66196d3f72b480f5309b343\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts index 86edd9992776d..c6aa0cdbc50c6 100644 --- a/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts +++ b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts @@ -48,40 +48,40 @@ test('create domain name resource', () => { cdkExpect(stack).to(haveResourceLike('Custom::AWS', { Properties: { Create: { - action: 'modifyVpcEndpointServiceConfiguration', - service: 'EC2', - parameters: { - PrivateDnsName: 'my-stuff.aws-cdk.dev', - ServiceId: { - Ref: 'VPCES3AE7D565', - }, - }, - physicalResourceId: { - id: 'VPCES', - }, + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"modifyVpcEndpointServiceConfiguration","parameters":{"ServiceId":"', + { + Ref: 'VPCES3AE7D565', + }, + '","PrivateDnsName":"my-stuff.aws-cdk.dev"},"physicalResourceId":{"id":"VPCES"}}', + ], + ], }, Update: { - action: 'modifyVpcEndpointServiceConfiguration', - service: 'EC2', - parameters: { - PrivateDnsName: 'my-stuff.aws-cdk.dev', - ServiceId: { - Ref: 'VPCES3AE7D565', - }, - }, - physicalResourceId: { - id: 'VPCES', - }, + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"modifyVpcEndpointServiceConfiguration","parameters":{"ServiceId":"', + { + Ref: 'VPCES3AE7D565', + }, + '","PrivateDnsName":"my-stuff.aws-cdk.dev"},"physicalResourceId":{"id":"VPCES"}}', + ], + ], }, Delete: { - action: 'modifyVpcEndpointServiceConfiguration', - service: 'EC2', - parameters: { - RemovePrivateDnsName: 'TRUE:BOOLEAN', - ServiceId: { - Ref: 'VPCES3AE7D565', - }, - }, + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"modifyVpcEndpointServiceConfiguration","parameters":{"ServiceId":"', + { + Ref: 'VPCES3AE7D565', + }, + '","RemovePrivateDnsName":true}}', + ], + ], }, }, DependsOn: [ @@ -94,22 +94,28 @@ test('create domain name resource', () => { cdkExpect(stack).to(haveResourceLike('Custom::AWS', { Properties: { Create: { - action: 'describeVpcEndpointServiceConfigurations', - service: 'EC2', - parameters: { - ServiceIds: [{ - Ref: 'VPCES3AE7D565', - }], - }, + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"describeVpcEndpointServiceConfigurations","parameters":{"ServiceIds":["', + { + Ref: 'VPCES3AE7D565', + }, + '"]},"physicalResourceId":{"id":"fcd2563479244a851a9a59af60831b01"}}', + ], + ], }, Update: { - action: 'describeVpcEndpointServiceConfigurations', - service: 'EC2', - parameters: { - ServiceIds: [{ - Ref: 'VPCES3AE7D565', - }], - }, + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"describeVpcEndpointServiceConfigurations","parameters":{"ServiceIds":["', + { + Ref: 'VPCES3AE7D565', + }, + '"]},"physicalResourceId":{"id":"fcd2563479244a851a9a59af60831b01"}}', + ], + ], }, }, DependsOn: [ @@ -167,64 +173,68 @@ test('create domain name resource', () => { cdkExpect(stack).to(haveResourceLike('Custom::AWS', { Properties: { Create: { - action: 'startVpcEndpointServicePrivateDnsVerification', - service: 'EC2', - parameters: { - ServiceId: { - Ref: 'VPCES3AE7D565', - }, - }, - physicalResourceId: { - id: { - 'Fn::Join': [ - ':', - [ - { - 'Fn::GetAtt': [ - 'EndpointDomainGetNames9E697ED2', - 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Name', - ], - }, - { - 'Fn::GetAtt': [ - 'EndpointDomainGetNames9E697ED2', - 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Value', - ], - }, + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"startVpcEndpointServicePrivateDnsVerification","parameters":{"ServiceId":"', + { + Ref: 'VPCES3AE7D565', + }, + '"},"physicalResourceId":{"id":"', + { + 'Fn::Join': [ + ':', + [ + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Name', + ], + }, + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Value', + ], + }, + ], ], - ], - }, - }, + }, + '"}}', + ], + ], }, Update: { - action: 'startVpcEndpointServicePrivateDnsVerification', - service: 'EC2', - parameters: { - ServiceId: { - Ref: 'VPCES3AE7D565', - }, - }, - physicalResourceId: { - id: { - 'Fn::Join': [ - ':', - [ - { - 'Fn::GetAtt': [ - 'EndpointDomainGetNames9E697ED2', - 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Name', - ], - }, - { - 'Fn::GetAtt': [ - 'EndpointDomainGetNames9E697ED2', - 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Value', - ], - }, + 'Fn::Join': [ + '', + [ + '{"service":"EC2","action":"startVpcEndpointServicePrivateDnsVerification","parameters":{"ServiceId":"', + { + Ref: 'VPCES3AE7D565', + }, + '"},"physicalResourceId":{"id":"', + { + 'Fn::Join': [ + ':', + [ + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Name', + ], + }, + { + 'Fn::GetAtt': [ + 'EndpointDomainGetNames9E697ED2', + 'ServiceConfigurations.0.PrivateDnsNameConfiguration.Value', + ], + }, + ], ], - ], - }, - }, + }, + '"}}', + ], + ], }, }, DependsOn: [ diff --git a/packages/@aws-cdk/aws-s3-assets/README.md b/packages/@aws-cdk/aws-s3-assets/README.md index aab4c46d9c44d..7a751410a2b22 100644 --- a/packages/@aws-cdk/aws-s3-assets/README.md +++ b/packages/@aws-cdk/aws-s3-assets/README.md @@ -88,8 +88,8 @@ The following example uses custom asset bundling to convert a markdown file to h [Example of using asset bundling](./test/integ.assets.bundling.lit.ts). -The bundling docker image (`image`) can either come from a registry (`BundlingDockerImage.fromRegistry`) -or it can be built from a `Dockerfile` located inside your project (`BundlingDockerImage.fromAsset`). +The bundling docker image (`image`) can either come from a registry (`DockerImage.fromRegistry`) +or it can be built from a `Dockerfile` located inside your project (`DockerImage.fromBuild`). You can set the `CDK_DOCKER` environment variable in order to provide a custom docker program to execute. This may sometime be needed when building in @@ -114,7 +114,7 @@ new assets.Asset(this, 'BundledAsset', { }, }, // Docker bundling fallback - image: BundlingDockerImage.fromRegistry('alpine'), + image: DockerImage.fromRegistry('alpine'), entrypoint: ['/bin/sh', '-c'], command: ['bundle'], }, @@ -124,6 +124,27 @@ new assets.Asset(this, 'BundledAsset', { Although optional, it's recommended to provide a local bundling method which can greatly improve performance. +If the bundling output contains a single archive file (zip or jar) it will be +uploaded to S3 as-is and will not be zipped. Otherwise the contents of the +output directory will be zipped and the zip file will be uploaded to S3. This +is the default behavior for `bundling.outputType` (`BundlingOutput.AUTO_DISCOVER`). + +Use `BundlingOutput.NOT_ARCHIVED` if the bundling output must always be zipped: + +```ts +const asset = new assets.Asset(this, 'BundledAsset', { + path: '/path/to/asset', + bundling: { + image: DockerImage.fromRegistry('alpine'), + command: ['command-that-produces-an-archive.sh'], + outputType: BundlingOutput.NOT_ARCHIVED, // Bundling output will be zipped even though it produces a single archive file. + }, +}); +``` + +Use `BundlingOutput.ARCHIVED` if the bundling output contains a single archive file and +you don't want it to be zippped. + ## CloudFormation Resource Metadata > NOTE: This section is relevant for authors of AWS Resource Constructs. diff --git a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts index 938778d1381f4..510834a61c634 100644 --- a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts +++ b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts @@ -1,4 +1,3 @@ -import * as fs from 'fs'; import * as path from 'path'; import * as assets from '@aws-cdk/assets'; import * as iam from '@aws-cdk/aws-iam'; @@ -13,8 +12,6 @@ import { toSymlinkFollow } from './compat'; // eslint-disable-next-line no-duplicate-imports, import/order import { Construct as CoreConstruct } from '@aws-cdk/core'; -const ARCHIVE_EXTENSIONS = ['.zip', '.jar']; - export interface AssetOptions extends assets.CopyOptions, cdk.AssetOptions { /** * A list of principals that should be able to read this asset from S3. @@ -139,17 +136,12 @@ export class Asset extends CoreConstruct implements cdk.IAsset { this.assetPath = staging.relativeStagedPath(stack); - const packaging = determinePackaging(staging.sourcePath); - - this.isFile = packaging === cdk.FileAssetPackaging.FILE; + this.isFile = staging.packaging === cdk.FileAssetPackaging.FILE; - // sets isZipArchive based on the type of packaging and file extension - this.isZipArchive = packaging === cdk.FileAssetPackaging.ZIP_DIRECTORY - ? true - : ARCHIVE_EXTENSIONS.some(ext => staging.sourcePath.toLowerCase().endsWith(ext)); + this.isZipArchive = staging.isArchive; const location = stack.synthesizer.addFileAsset({ - packaging, + packaging: staging.packaging, sourceHash: this.sourceHash, fileName: this.assetPath, }); @@ -210,19 +202,3 @@ export class Asset extends CoreConstruct implements cdk.IAsset { this.bucket.grantRead(grantee); } } - -function determinePackaging(assetPath: string): cdk.FileAssetPackaging { - if (!fs.existsSync(assetPath)) { - throw new Error(`Cannot find asset at ${assetPath}`); - } - - if (fs.statSync(assetPath).isDirectory()) { - return cdk.FileAssetPackaging.ZIP_DIRECTORY; - } - - if (fs.statSync(assetPath).isFile()) { - return cdk.FileAssetPackaging.FILE; - } - - throw new Error(`Asset ${assetPath} is expected to be either a directory or a regular file`); -} diff --git a/packages/@aws-cdk/aws-s3-deployment/package.json b/packages/@aws-cdk/aws-s3-deployment/package.json index fadd40d30149a..859b206963e0e 100644 --- a/packages/@aws-cdk/aws-s3-deployment/package.json +++ b/packages/@aws-cdk/aws-s3-deployment/package.json @@ -78,6 +78,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@types/jest": "^26.0.20", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts b/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts index 5cb7cdc1e7cdf..9639af37a2aa8 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts +++ b/packages/@aws-cdk/aws-s3-deployment/test/bucket-deployment.test.ts @@ -5,10 +5,14 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as s3deploy from '../lib'; /* eslint-disable max-len */ +const s3GrantWriteCtx = { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }; + test('deploy from local directory asset', () => { // GIVEN const stack = new cdk.Stack(); @@ -449,9 +453,9 @@ test('fails if distribution paths provided but not distribution ID', () => { }); -test('lambda execution role gets permissions to read from the source bucket and read/write in destination', () => { +testFutureBehavior('lambda execution role gets permissions to read from the source bucket and read/write in destination', s3GrantWriteCtx, cdk.App, (app) => { // GIVEN - const stack = new cdk.Stack(); + const stack = new cdk.Stack(app); const source = new s3.Bucket(stack, 'Source'); const bucket = new s3.Bucket(stack, 'Dest'); @@ -501,7 +505,7 @@ test('lambda execution role gets permissions to read from the source bucket and 's3:GetBucket*', 's3:List*', 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], Effect: 'Allow', diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index 12ae88bb02d2e..d6a45e77cb7e1 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -120,6 +120,18 @@ bucket.grantReadWrite(lambda); Will give the Lambda's execution role permissions to read and write from the bucket. +## AWS Foundational Security Best Practices + +### Enforcing SSL + +To require all requests use Secure Socket Layer (SSL): + +```ts +const bucket = new Bucket(this, 'Bucket', { + enforceSSL: true +}); +``` + ## Sharing buckets between stacks To use a bucket in a different stack in the same CDK application, pass the object to the other stack: diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 4323f47daf575..630aa9f02bfcc 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -1061,6 +1061,14 @@ export interface BucketProps { */ readonly encryptionKey?: kms.IKey; + /** + * Enforces SSL for requests. S3.5 of the AWS Foundational Security Best Practices Regarding S3. + * @see https://docs.aws.amazon.com/config/latest/developerguide/s3-bucket-ssl-requests-only.html + * + * @default false + */ + readonly enforceSSL?: boolean; + /** * Specifies whether Amazon S3 should use an S3 Bucket Key with server-side * encryption using KMS (SSE-KMS) for new objects in the bucket. @@ -1357,6 +1365,11 @@ export class Bucket extends BucketBase { this.disallowPublicAccess = props.blockPublicAccess && props.blockPublicAccess.blockPublicPolicy; this.accessControl = props.accessControl; + // Enforce AWS Foundational Security Best Practice + if (props.enforceSSL) { + this.enforceSSLStatement(); + } + if (props.serverAccessLogsBucket instanceof Bucket) { props.serverAccessLogsBucket.allowLogDelivery(); } @@ -1479,6 +1492,22 @@ export class Bucket extends BucketBase { this.inventories.push(inventory); } + /** + * Adds an iam statement to enforce SSL requests only. + */ + private enforceSSLStatement() { + const statement = new iam.PolicyStatement({ + actions: ['s3:*'], + conditions: { + Bool: { 'aws:SecureTransport': 'false' }, + }, + effect: iam.Effect.DENY, + resources: [this.arnForObjects('*')], + principals: [new iam.AnyPrincipal()], + }); + this.addToResourcePolicy(statement); + } + private validateBucketName(physicalName: string): void { const bucketName = physicalName; if (!bucketName || Token.isUnresolved(bucketName)) { @@ -1801,6 +1830,7 @@ export class Bucket extends BucketBase { const provider = CustomResourceProvider.getOrCreateProvider(this, AUTO_DELETE_OBJECTS_RESOURCE_TYPE, { codeDirectory: path.join(__dirname, 'auto-delete-objects-handler'), runtime: CustomResourceProviderRuntime.NODEJS_12, + description: `Lambda function for auto-deleting objects in ${this.bucketName} S3 bucket.`, }); // Use a bucket policy to allow the custom resource to delete diff --git a/packages/@aws-cdk/aws-s3/test/bucket.test.ts b/packages/@aws-cdk/aws-s3/test/bucket.test.ts index 0fb1bffa3726f..936c807e8da4a 100644 --- a/packages/@aws-cdk/aws-s3/test/bucket.test.ts +++ b/packages/@aws-cdk/aws-s3/test/bucket.test.ts @@ -5,12 +5,14 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; +import { testFutureBehavior, testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import * as s3 from '../lib'; // to make it easy to copy & paste from output: /* eslint-disable quote-props */ +const s3GrantWriteCtx = { [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true }; + describe('bucket', () => { test('default bucket', () => { const stack = new cdk.Stack(); @@ -277,6 +279,58 @@ describe('bucket', () => { }); + test('enforceSsl can be enabled', () => { + const stack = new cdk.Stack(); + new s3.Bucket(stack, 'MyBucket', { enforceSSL: true }); + + expect(stack).toMatchTemplate({ + 'Resources': { + 'MyBucketF68F3FF0': { + 'Type': 'AWS::S3::Bucket', + 'UpdateReplacePolicy': 'Retain', + 'DeletionPolicy': 'Retain', + }, + 'MyBucketPolicyE7FBAC7B': { + 'Type': 'AWS::S3::BucketPolicy', + 'Properties': { + 'Bucket': { + 'Ref': 'MyBucketF68F3FF0', + }, + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 's3:*', + 'Condition': { + 'Bool': { + 'aws:SecureTransport': 'false', + }, + }, + 'Effect': 'Deny', + 'Principal': '*', + 'Resource': { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'MyBucketF68F3FF0', + 'Arn', + ], + }, + '/*', + ], + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + }, + }, + }, + }); + }); + test('bucketKeyEnabled can be enabled', () => { const stack = new cdk.Stack(); @@ -762,8 +816,8 @@ describe('bucket', () => { }); describe('grantReadWrite', () => { - test('can be used to grant reciprocal permissions to an identity', () => { - const stack = new cdk.Stack(); + testFutureBehavior('can be used to grant reciprocal permissions to an identity', s3GrantWriteCtx, cdk.App, (app) => { + const stack = new cdk.Stack(app); const bucket = new s3.Bucket(stack, 'MyBucket'); const user = new iam.User(stack, 'MyUser'); bucket.grantReadWrite(user); @@ -789,7 +843,7 @@ describe('bucket', () => { 's3:GetBucket*', 's3:List*', 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], 'Effect': 'Allow', @@ -1052,12 +1106,7 @@ describe('bucket', () => { }); }); - test('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', () => { - const app = new cdk.App({ - context: { - [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true, - }, - }); + testFutureBehavior('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', s3GrantWriteCtx, cdk.App, (app) => { const stack = new cdk.Stack(app, 'Stack'); const bucket = new s3.Bucket(stack, 'MyBucket'); const user = new iam.User(stack, 'MyUser'); @@ -1095,8 +1144,8 @@ describe('bucket', () => { }); describe('grantWrite', () => { - test('with KMS key has appropriate permissions for multipart uploads', () => { - const stack = new cdk.Stack(); + testFutureBehavior('with KMS key has appropriate permissions for multipart uploads', s3GrantWriteCtx, cdk.App, (app) => { + const stack = new cdk.Stack(app); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS }); const user = new iam.User(stack, 'MyUser'); bucket.grantWrite(user); @@ -1107,7 +1156,7 @@ describe('bucket', () => { { 'Action': [ 's3:DeleteObject*', - 's3:PutObject*', + 's3:PutObject', 's3:Abort*', ], 'Effect': 'Allow', @@ -1163,12 +1212,7 @@ describe('bucket', () => { }); - test('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', () => { - const app = new cdk.App({ - context: { - [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true, - }, - }); + testFutureBehavior('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', s3GrantWriteCtx, cdk.App, (app) => { const stack = new cdk.Stack(app, 'Stack'); const bucket = new s3.Bucket(stack, 'MyBucket'); const user = new iam.User(stack, 'MyUser'); @@ -1203,12 +1247,7 @@ describe('bucket', () => { }); describe('grantPut', () => { - test('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', () => { - const app = new cdk.App({ - context: { - [cxapi.S3_GRANT_WRITE_WITHOUT_ACL]: true, - }, - }); + testFutureBehavior('does not grant PutObjectAcl when the S3_GRANT_WRITE_WITHOUT_ACL feature is enabled', s3GrantWriteCtx, cdk.App, (app) => { const stack = new cdk.Stack(app, 'Stack'); const bucket = new s3.Bucket(stack, 'MyBucket'); const user = new iam.User(stack, 'MyUser'); @@ -1238,8 +1277,8 @@ describe('bucket', () => { }); }); - test('more grants', () => { - const stack = new cdk.Stack(); + testFutureBehavior('more grants', s3GrantWriteCtx, cdk.App, (app) => { + const stack = new cdk.Stack(app); const bucket = new s3.Bucket(stack, 'MyBucket', { encryption: s3.BucketEncryption.KMS }); const putter = new iam.User(stack, 'Putter'); const writer = new iam.User(stack, 'Writer'); @@ -1252,8 +1291,8 @@ describe('bucket', () => { const resources = SynthUtils.synthesize(stack).template.Resources; const actions = (id: string) => resources[id].Properties.PolicyDocument.Statement[0].Action; - expect(actions('WriterDefaultPolicyDC585BCE')).toEqual(['s3:DeleteObject*', 's3:PutObject*', 's3:Abort*']); - expect(actions('PutterDefaultPolicyAB138DD3')).toEqual(['s3:PutObject*', 's3:Abort*']); + expect(actions('WriterDefaultPolicyDC585BCE')).toEqual(['s3:DeleteObject*', 's3:PutObject', 's3:Abort*']); + expect(actions('PutterDefaultPolicyAB138DD3')).toEqual(['s3:PutObject', 's3:Abort*']); expect(actions('DeleterDefaultPolicyCD33B8A0')).toEqual('s3:DeleteObject*'); }); diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json index d9f263a8d840d..831d072339649 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json @@ -148,7 +148,19 @@ "Arn" ] }, - "Runtime": "nodejs12.x" + "Runtime": "nodejs12.x", + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting objects in ", + { + "Ref": "Bucket83908E77" + }, + " S3 bucket." + ] + ] + } }, "DependsOn": [ "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.ts b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.ts index 83243212409d7..8052dd340d888 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.ts +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.ts @@ -17,7 +17,7 @@ class TestStack extends Stack { // Put objects in the bucket to ensure auto delete works as expected const serviceToken = CustomResourceProvider.getOrCreate(this, PUT_OBJECTS_RESOURCE_TYPE, { codeDirectory: path.join(__dirname, 'put-objects-handler'), - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, policyStatements: [{ Effect: 'Allow', Action: 's3:PutObject', diff --git a/packages/@aws-cdk/aws-sam/package.json b/packages/@aws-cdk/aws-sam/package.json index 6e76bd0098faf..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.1" + "ts-jest": "^26.5.3" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret-name-parsed.ts b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret-name-parsed.ts index 2d245cc6e3e01..801077c5c8494 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret-name-parsed.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret-name-parsed.ts @@ -21,7 +21,7 @@ class SecretsManagerStack extends cdk.Stack { const resourceType = 'Custom::IntegVerificationSecretNameMatches'; const serviceToken = cdk.CustomResourceProvider.getOrCreate(this, resourceType, { codeDirectory: path.join(__dirname, 'integ.secret-name-parsed.handler'), - runtime: cdk.CustomResourceProviderRuntime.NODEJS_12, + runtime: cdk.CustomResourceProviderRuntime.NODEJS_12_X, policyStatements: [{ Effect: 'Allow', Resource: secrets.map(s => s.secretArn), diff --git a/packages/@aws-cdk/aws-ses-actions/test/integ.actions.expected.json b/packages/@aws-cdk/aws-ses-actions/test/integ.actions.expected.json index b58b770bcd426..4379649e02d69 100644 --- a/packages/@aws-cdk/aws-ses-actions/test/integ.actions.expected.json +++ b/packages/@aws-cdk/aws-ses-actions/test/integ.actions.expected.json @@ -40,13 +40,13 @@ "Code": { "ZipFile": "exports.handler = async (event) => event;" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "FunctionServiceRole675BB04A", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -341,16 +341,51 @@ "Type": "AWS::Lambda::Function", "Properties": { "Code": { - "ZipFile": "exports.handler = function dropSpamCode(event, _, callback) {\n console.log('Spam filter');\n const sesNotification = event.Records[0].ses;\n console.log('SES Notification:\\n', JSON.stringify(sesNotification, null, 2));\n // Check if any spam check failed\n if (sesNotification.receipt.spfVerdict.status === 'FAIL'\n || sesNotification.receipt.dkimVerdict.status === 'FAIL'\n || sesNotification.receipt.spamVerdict.status === 'FAIL'\n || sesNotification.receipt.virusVerdict.status === 'FAIL') {\n console.log('Dropping spam');\n // Stop processing rule set, dropping message\n callback(null, { disposition: 'STOP_RULE_SET' });\n }\n else {\n callback(null, null);\n }\n}" + "S3Bucket": { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3Bucket6AFCBA5F" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086" + } + ] + } + ] + } + ] + ] + } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4" @@ -372,5 +407,19 @@ } } } + }, + "Parameters": { + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3Bucket6AFCBA5F": { + "Type": "String", + "Description": "S3 bucket for asset \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + }, + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086": { + "Type": "String", + "Description": "S3 key for asset version \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + }, + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34ArtifactHash6BE57680": { + "Type": "String", + "Description": "Artifact hash for asset \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ses/lib/drop-spam-handler/index.ts b/packages/@aws-cdk/aws-ses/lib/drop-spam-handler/index.ts new file mode 100644 index 0000000000000..76a639acdf50e --- /dev/null +++ b/packages/@aws-cdk/aws-ses/lib/drop-spam-handler/index.ts @@ -0,0 +1,22 @@ +/* eslint-disable no-console */ + +// Adapted from https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-lambda-example-functions.html +export async function handler(event: AWSLambda.SESEvent): Promise<{ disposition: string } | null> { + console.log('Spam filter'); + + const sesNotification = event.Records[0].ses; + console.log('SES Notification: %j', sesNotification); + + // Check if any spam check failed + if (sesNotification.receipt.spfVerdict.status === 'FAIL' + || sesNotification.receipt.dkimVerdict.status === 'FAIL' + || sesNotification.receipt.spamVerdict.status === 'FAIL' + || sesNotification.receipt.virusVerdict.status === 'FAIL') { + console.log('Dropping spam'); + + // Stop processing rule set, dropping message + return { disposition: 'STOP_RULE_SET' }; + } + + return null; +} diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts index 5b6a276929c8e..7f6f1fe914a6b 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts @@ -1,3 +1,4 @@ +import * as path from 'path'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import { Aws, IResource, Lazy, Resource } from '@aws-cdk/core'; @@ -175,9 +176,9 @@ export class DropSpamReceiptRule extends CoreConstruct { super(scope, id); const fn = new lambda.SingletonFunction(this, 'Function', { - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', - code: lambda.Code.fromInline(`exports.handler = ${dropSpamCode}`), + code: lambda.Code.fromAsset(path.join(__dirname, 'drop-spam-handler')), uuid: '224e77f9-a32e-4b4d-ac32-983477abba16', }); @@ -203,25 +204,3 @@ export class DropSpamReceiptRule extends CoreConstruct { }); } } - -// Adapted from https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-lambda-example-functions.html -/* eslint-disable no-console */ -function dropSpamCode(event: any, _: any, callback: any) { - console.log('Spam filter'); - - const sesNotification = event.Records[0].ses; - console.log('SES Notification:\n', JSON.stringify(sesNotification, null, 2)); - - // Check if any spam check failed - if (sesNotification.receipt.spfVerdict.status === 'FAIL' - || sesNotification.receipt.dkimVerdict.status === 'FAIL' - || sesNotification.receipt.spamVerdict.status === 'FAIL' - || sesNotification.receipt.virusVerdict.status === 'FAIL') { - console.log('Dropping spam'); - - // Stop processing rule set, dropping message - callback(null, { disposition: 'STOP_RULE_SET' }); - } else { - callback(null, null); - } -} diff --git a/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json b/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json index b4cdf79e60d44..2bb22f110b951 100644 --- a/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json +++ b/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json @@ -97,16 +97,51 @@ "Type": "AWS::Lambda::Function", "Properties": { "Code": { - "ZipFile": "exports.handler = function dropSpamCode(event, _, callback) {\n console.log('Spam filter');\n const sesNotification = event.Records[0].ses;\n console.log('SES Notification:\\n', JSON.stringify(sesNotification, null, 2));\n // Check if any spam check failed\n if (sesNotification.receipt.spfVerdict.status === 'FAIL'\n || sesNotification.receipt.dkimVerdict.status === 'FAIL'\n || sesNotification.receipt.spamVerdict.status === 'FAIL'\n || sesNotification.receipt.virusVerdict.status === 'FAIL') {\n console.log('Dropping spam');\n // Stop processing rule set, dropping message\n callback(null, { disposition: 'STOP_RULE_SET' });\n }\n else {\n callback(null, null);\n }\n}" + "S3Bucket": { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3Bucket6AFCBA5F" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086" + } + ] + } + ] + } + ] + ] + } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4" @@ -150,5 +185,19 @@ } } } + }, + "Parameters": { + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3Bucket6AFCBA5F": { + "Type": "String", + "Description": "S3 bucket for asset \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + }, + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34S3VersionKey02BA9086": { + "Type": "String", + "Description": "S3 key for asset version \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + }, + "AssetParameters96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34ArtifactHash6BE57680": { + "Type": "String", + "Description": "Artifact hash for asset \"96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34\"" + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-signer/README.md b/packages/@aws-cdk/aws-signer/README.md index 5482a0b23c900..925261fd4be52 100644 --- a/packages/@aws-cdk/aws-signer/README.md +++ b/packages/@aws-cdk/aws-signer/README.md @@ -9,12 +9,55 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +AWS Signer is a fully managed code-signing service to ensure the trust and integrity of your code. Organizations validate code against +a digital signature to confirm that the code is unaltered and from a trusted publisher. For more information, see [What Is AWS +Signer?](https://docs.aws.amazon.com/signer/latest/developerguide/Welcome.html) + +## Table of Contents + +- [Signing Platform](#signing-platform) +- [Signing Profile](#signing-profile) + +## Signing Platform + +A signing platform is a predefined set of instructions that specifies the signature format and signing algorithms that AWS Signer should use +to sign a zip file. For more information go to [Signing Platforms in AWS Signer](https://docs.aws.amazon.com/signer/latest/developerguide/gs-platform.html). + +AWS Signer provides a pre-defined set of signing platforms. They are available in the CDK as - ```ts -import signer = require('@aws-cdk/aws-signer'); +Platform.AWS_IOT_DEVICE_MANAGEMENT_SHA256_ECDSA +Platform.AWS_LAMBDA_SHA384_ECDSA +Platform.AMAZON_FREE_RTOS_TI_CC3220SF +Platform.AMAZON_FREE_RTOS_DEFAULT ``` + +## Signing Profile + +A signing profile is a code-signing template that can be used to pre-define the signature specifications for a signing job. +A signing profile includes a signing platform to designate the file type to be signed, the signature format, and the signature algorithms. +For more information, visit [Signing Profiles in AWS Signer](https://docs.aws.amazon.com/signer/latest/developerguide/gs-profile.html). + +The following code sets up a signing profile for signing lambda code bundles - + +```ts +import * as signer from '@aws-cdk/aws-signer'; + +const signingProfile = new signer.SigningProfile(this, 'SigningProfile', { + platform: signer.Platform.AWS_LAMBDA_SHA384_ECDSA, +} ); +``` + +A signing profile is valid by default for 135 months. This can be modified by specifying the `signatureValidityPeriod` property. diff --git a/packages/@aws-cdk/aws-signer/lib/index.ts b/packages/@aws-cdk/aws-signer/lib/index.ts index 9c56379e86c19..090dec21fac3b 100644 --- a/packages/@aws-cdk/aws-signer/lib/index.ts +++ b/packages/@aws-cdk/aws-signer/lib/index.ts @@ -1,2 +1,3 @@ // AWS::Signer CloudFormation Resources: export * from './signer.generated'; +export * from './signing-profile'; diff --git a/packages/@aws-cdk/aws-signer/lib/signing-profile.ts b/packages/@aws-cdk/aws-signer/lib/signing-profile.ts new file mode 100644 index 0000000000000..8a0d14c3d194a --- /dev/null +++ b/packages/@aws-cdk/aws-signer/lib/signing-profile.ts @@ -0,0 +1,178 @@ +import { Duration, IResource, Resource, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnSigningProfile } from './signer.generated'; + +/** + * Platforms that are allowed with signing config. + * @see https://docs.aws.amazon.com/signer/latest/developerguide/gs-platform.html + */ +export class Platform { + /** + * Specification of signature format and signing algorithms for AWS IoT Device. + */ + public static readonly AWS_IOT_DEVICE_MANAGEMENT_SHA256_ECDSA = new Platform('AWSIoTDeviceManagement-SHA256-ECDSA'); + + /** + * Specification of signature format and signing algorithms for AWS Lambda. + */ + public static readonly AWS_LAMBDA_SHA384_ECDSA = new Platform('AWSLambda-SHA384-ECDSA'); + + /** + * Specification of signature format and signing algorithms with + * SHA1 hash and RSA encryption for Amazon FreeRTOS. + */ + public static readonly AMAZON_FREE_RTOS_TI_CC3220SF = new Platform('AmazonFreeRTOS-TI-CC3220SF'); + + /** + * Specification of signature format and signing algorithms with + * SHA256 hash and ECDSA encryption for Amazon FreeRTOS. + */ + public static readonly AMAZON_FREE_RTOS_DEFAULT = new Platform('AmazonFreeRTOS-Default'); + + /** + * The id of signing platform. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-signer-signingprofile.html#cfn-signer-signingprofile-platformid + */ + public readonly platformId: string; + + private constructor(platformId: string) { + this.platformId = platformId; + } +} + +/** + * A Signer Profile + */ +export interface ISigningProfile extends IResource { + /** + * The ARN of the signing profile. + * @attribute + */ + readonly signingProfileArn: string; + + /** + * The name of signing profile. + * @attribute ProfileName + */ + readonly signingProfileName: string; + + /** + * The version of signing profile. + * @attribute ProfileVersion + */ + readonly signingProfileVersion: string; + + /** + * The ARN of signing profile version. + * @attribute ProfileVersionArn + */ + readonly signingProfileVersionArn: string; +} + +/** + * Construction properties for a Signing Profile object + */ +export interface SigningProfileProps { + /** + * The Signing Platform available for signing profile. + * @see https://docs.aws.amazon.com/signer/latest/developerguide/gs-platform.html + */ + readonly platform: Platform; + + /** + * The validity period for signatures generated using + * this signing profile. + * + * @default - 135 months + */ + readonly signatureValidity?: Duration; + + /** + * Physical name of this Signing Profile. + * + * @default - Assigned by CloudFormation (recommended). + */ + readonly signingProfileName?: string; +} + +/** + * A reference to a Signing Profile + */ +export interface SigningProfileAttributes { + /** + * The name of signing profile. + */ + readonly signingProfileName: string; + + /** + * The version of signing profile. + */ + readonly signingProfileVersion: string; +} + +/** + * Defines a Signing Profile. + * + * @resource AWS::Signer::SigningProfile + */ +export class SigningProfile extends Resource implements ISigningProfile { + /** + * Creates a Signing Profile construct that represents an external Signing Profile. + * + * @param scope The parent creating construct (usually `this`). + * @param id The construct's name. + * @param attrs A `SigningProfileAttributes` object. + */ + public static fromSigningProfileAttributes( scope: Construct, id: string, attrs: SigningProfileAttributes): ISigningProfile { + class Import extends Resource implements ISigningProfile { + public readonly signingProfileArn: string; + public readonly signingProfileName = attrs.signingProfileName; + public readonly signingProfileVersion = attrs.signingProfileVersion; + public readonly signingProfileVersionArn: string; + + constructor(signingProfileArn: string, signingProfileProfileVersionArn: string) { + super(scope, id); + this.signingProfileArn = signingProfileArn; + this.signingProfileVersionArn = signingProfileProfileVersionArn; + } + } + const signingProfileArn = Stack.of(scope).formatArn({ + service: 'signer', + resource: '', + resourceName: `/signing-profiles/${attrs.signingProfileName}`, + }); + const SigningProfileVersionArn = Stack.of(scope).formatArn({ + service: 'signer', + resource: '', + resourceName: `/signing-profiles/${attrs.signingProfileName}/${attrs.signingProfileVersion}`, + }); + return new Import(signingProfileArn, SigningProfileVersionArn); + } + + public readonly signingProfileArn: string; + public readonly signingProfileName: string; + public readonly signingProfileVersion: string; + public readonly signingProfileVersionArn: string; + + constructor(scope: Construct, id: string, props: SigningProfileProps) { + super(scope, id, { + physicalName: props.signingProfileName, + }); + + const resource = new CfnSigningProfile( this, 'Resource', { + platformId: props.platform.platformId, + signatureValidityPeriod: props.signatureValidity ? { + type: 'DAYS', + value: props.signatureValidity?.toDays(), + } : { + type: 'MONTHS', + value: 135, + }, + } ); + + this.signingProfileArn = resource.attrArn; + this.signingProfileName = resource.attrProfileName; + this.signingProfileVersion = resource.attrProfileVersion; + this.signingProfileVersionArn = resource.attrProfileVersionArn; + } +} diff --git a/packages/@aws-cdk/aws-signer/package.json b/packages/@aws-cdk/aws-signer/package.json index 40a8f5872b5b1..f01a984dfdd28 100644 --- a/packages/@aws-cdk/aws-signer/package.json +++ b/packages/@aws-cdk/aws-signer/package.json @@ -79,16 +79,18 @@ "pkglint": "0.0.0" }, "dependencies": { - "@aws-cdk/core": "0.0.0" + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" }, "peerDependencies": { - "@aws-cdk/core": "0.0.0" + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-signer/test/signer.test.ts b/packages/@aws-cdk/aws-signer/test/signer.test.ts deleted file mode 100644 index e394ef336bfb4..0000000000000 --- a/packages/@aws-cdk/aws-signer/test/signer.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assert/jest'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/aws-signer/test/signing-profile.test.ts b/packages/@aws-cdk/aws-signer/test/signing-profile.test.ts new file mode 100644 index 0000000000000..6148a6be70bda --- /dev/null +++ b/packages/@aws-cdk/aws-signer/test/signing-profile.test.ts @@ -0,0 +1,115 @@ +import '@aws-cdk/assert/jest'; +import * as cdk from '@aws-cdk/core'; +import * as signer from '../lib'; + +let app: cdk.App; +let stack: cdk.Stack; +beforeEach( () => { + app = new cdk.App( {} ); + stack = new cdk.Stack( app ); +} ); + +describe('signing profile', () => { + test( 'default', () => { + const platform = signer.Platform.AWS_LAMBDA_SHA384_ECDSA; + new signer.SigningProfile( stack, 'SigningProfile', { platform } ); + + expect(stack).toHaveResource('AWS::Signer::SigningProfile', { + PlatformId: platform.platformId, + SignatureValidityPeriod: { + Type: 'MONTHS', + Value: 135, + }, + }); + }); + + test( 'default with signature validity period', () => { + const platform = signer.Platform.AWS_LAMBDA_SHA384_ECDSA; + new signer.SigningProfile( stack, 'SigningProfile', { + platform, + signatureValidity: cdk.Duration.days( 7 ), + } ); + + expect(stack).toHaveResource('AWS::Signer::SigningProfile', { + PlatformId: platform.platformId, + SignatureValidityPeriod: { + Type: 'DAYS', + Value: 7, + }, + }); + }); + + test( 'default with some tags', () => { + const platform = signer.Platform.AWS_LAMBDA_SHA384_ECDSA; + const signing = new signer.SigningProfile( stack, 'SigningProfile', { platform } ); + + cdk.Tags.of(signing).add('tag1', 'value1'); + cdk.Tags.of(signing).add('tag2', 'value2'); + cdk.Tags.of(signing).add('tag3', ''); + + expect(stack).toHaveResource('AWS::Signer::SigningProfile', { + PlatformId: platform.platformId, + SignatureValidityPeriod: { + Type: 'MONTHS', + Value: 135, + }, + Tags: [ + { + Key: 'tag1', + Value: 'value1', + }, + { + Key: 'tag2', + Value: 'value2', + }, + { + Key: 'tag3', + Value: '', + }, + ], + }); + }); + + describe('import', () => { + test('from signingProfileProfileName and signingProfileProfileVersion', () => { + const signingProfileName = 'test'; + const signingProfileVersion = 'xxxxxxxx'; + const signingProfile = signer.SigningProfile.fromSigningProfileAttributes(stack, 'Imported', { + signingProfileName, + signingProfileVersion, + }); + + expect(stack.resolve(signingProfile.signingProfileArn)).toStrictEqual( + { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':signer:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + `://signing-profiles/${signingProfileName}`, + ], + ], + }, + ); + expect(stack.resolve(signingProfile.signingProfileVersionArn)).toStrictEqual({ + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':signer:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + `://signing-profiles/${signingProfileName}/${signingProfileVersion}`, + ], + ], + }); + expect(stack).toMatchTemplate({}); + }); + } ); +}); diff --git a/packages/@aws-cdk/aws-sns-subscriptions/README.md b/packages/@aws-cdk/aws-sns-subscriptions/README.md index 3e047f2e802b6..c2bb95910d125 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/README.md +++ b/packages/@aws-cdk/aws-sns-subscriptions/README.md @@ -78,7 +78,7 @@ import * as subscriptions from '@aws-cdk/aws-sns-subscriptions'; const myFunction = new lambda.Function(this, 'Echo', { handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_12_X, code: lambda.Code.fromInline(`exports.handler = ${handler.toString()}`) }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index e7616028054fd..d7c2d1498394d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -188,8 +188,8 @@ const convertToSeconds = new tasks.EvaluateExpression(this, 'Convert to seconds' const createMessage = new tasks.EvaluateExpression(this, 'Create message', { // Note: this is a string inside a string. - expression: '`Now waiting ${$.waitSeconds} seconds...`', - runtime: lambda.Runtime.NODEJS_10_X, + expression: '`Now waiting ${$.waitSeconds} seconds...`', + runtime: lambda.Runtime.NODEJS_14_X, resultPath: '$.message', }); @@ -212,9 +212,8 @@ new sfn.StateMachine(this, 'StateMachine', { ``` The `EvaluateExpression` supports a `runtime` prop to specify the Lambda -runtime to use to evaluate the expression. Currently, the only runtime -supported is `lambda.Runtime.NODEJS_10_X`. - +runtime to use to evaluate the expression. Currently, only runtimes +of the Node.js family are supported. ## Athena diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-nodejs10.x-handler/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-nodejs-handler/index.ts similarity index 100% rename from packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-nodejs10.x-handler/index.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/lib/eval-nodejs-handler/index.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts index 45457a1c377c8..64c25d5e3dd3a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts @@ -20,7 +20,7 @@ export interface EvaluateExpressionProps extends sfn.TaskStateBaseProps { /** * The runtime language to use to evaluate the expression. * - * @default lambda.Runtime.NODEJS_10_X + * @default lambda.Runtime.NODEJS_14_X */ readonly runtime?: lambda.Runtime; } @@ -58,7 +58,7 @@ export class EvaluateExpression extends sfn.TaskStateBase { constructor(scope: Construct, id: string, private readonly props: EvaluateExpressionProps) { super(scope, id, props); - this.evalFn = createEvalFn(this.props.runtime || lambda.Runtime.NODEJS_10_X, this); + this.evalFn = createEvalFn(this.props.runtime ?? lambda.Runtime.NODEJS_14_X, this); this.taskPolicies = [ new iam.PolicyStatement({ @@ -97,17 +97,18 @@ export class EvaluateExpression extends sfn.TaskStateBase { } function createEvalFn(runtime: lambda.Runtime, scope: Construct) { - const code = lambda.Code.asset(path.join(__dirname, `eval-${runtime.name}-handler`)); const lambdaPurpose = 'Eval'; switch (runtime) { + case lambda.Runtime.NODEJS_14_X: + case lambda.Runtime.NODEJS_12_X: case lambda.Runtime.NODEJS_10_X: return new lambda.SingletonFunction(scope, 'EvalFunction', { runtime, handler: 'index.handler', uuid: 'a0d2ce44-871b-4e74-87a1-f5e63d7c3bdc', lambdaPurpose, - code, + code: lambda.Code.fromAsset(path.join(__dirname, 'eval-nodejs-handler')), }); // TODO: implement other runtimes default: diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/update-endpoint.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/update-endpoint.ts index 4a4e068d816d0..4ff5bf4d19bb3 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/update-endpoint.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/update-endpoint.ts @@ -73,6 +73,13 @@ export class SageMakerUpdateEndpoint extends sfn.TaskStateBase { // SageMaker uses lowercase for resource name in the arn resourceName: sfn.JsonPath.isEncodedJsonPath(this.props.endpointName) ? '*' : `${this.props.endpointName.toLowerCase()}`, }), + stack.formatArn({ + service: 'sagemaker', + resource: 'endpoint-config', + // If the endpointConfig name comes from input, we cannot target the policy to a particular ARN prefix reliably. + // SageMaker uses lowercase for resource name in the arn + resourceName: sfn.JsonPath.isEncodedJsonPath(this.props.endpointConfigName) ? '*' : `${this.props.endpointConfigName.toLowerCase()}`, + }), ], }), ]; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs10.x-handler.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs-handler.test.ts similarity index 96% rename from packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs10.x-handler.test.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs-handler.test.ts index d42be11d7aaa4..8d69e9d3b8105 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs10.x-handler.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eval-nodejs-handler.test.ts @@ -1,5 +1,5 @@ import { Event } from '../lib'; -import { handler } from '../lib/eval-nodejs10.x-handler'; +import { handler } from '../lib/eval-nodejs-handler'; test('with numbers', async () => { // GIVEN diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts index 679e817dfbead..7a2aa196b3de2 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts @@ -34,7 +34,7 @@ test('Eval with Node.js', () => { }); expect(stack).toHaveResource('AWS::Lambda::Function', { - Runtime: 'nodejs10.x', + Runtime: 'nodejs14.x', }); }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json index efdf3878e67e2..c48b04a826783 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3BucketA16CB30E" + "Ref": "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3Bucket743A2950" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3VersionKey102DBBD9" + "Ref": "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3VersionKey2DBCB833" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3VersionKey102DBBD9" + "Ref": "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3VersionKey2DBCB833" } ] } @@ -72,14 +72,14 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "Evala0d2ce44871b4e7487a1f5e63d7c3bdcServiceRoleDC85DDD3", "Arn" ] }, - "Runtime": "nodejs10.x" + "Handler": "index.handler", + "Runtime": "nodejs14.x" }, "DependsOn": [ "Evala0d2ce44871b4e7487a1f5e63d7c3bdcServiceRoleDC85DDD3" @@ -185,17 +185,17 @@ } }, "Parameters": { - "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3BucketA16CB30E": { + "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3Bucket743A2950": { "Type": "String", - "Description": "S3 bucket for asset \"640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0\"" + "Description": "S3 bucket for asset \"bc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626b\"" }, - "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0S3VersionKey102DBBD9": { + "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bS3VersionKey2DBCB833": { "Type": "String", - "Description": "S3 key for asset version \"640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0\"" + "Description": "S3 key for asset version \"bc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626b\"" }, - "AssetParameters640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0ArtifactHash43D553D7": { + "AssetParametersbc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626bArtifactHashAD6C554B": { "Type": "String", - "Description": "Artifact hash for asset \"640b7d3e1a6ff78c1cad25c2d7396d04c74d6eee31b116f4c86d910338d480d0\"" + "Description": "Artifact hash for asset \"bc320c7bd6a0eba90db647aa586cf65548560a54c141153fdc12f22eb3b2626b\"" } }, "Outputs": { @@ -205,4 +205,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json index 1e8f55ca57056..c163ebd11cf77 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json @@ -465,26 +465,48 @@ { "Action": "sagemaker:updateEndpoint", "Effect": "Allow", - "Resource": { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":sagemaker:", - { - "Ref": "AWS::Region" - }, - ":", - { - "Ref": "AWS::AccountId" - }, - ":endpoint/*" + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":sagemaker:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":endpoint/*" + ] ] - ] - } + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":sagemaker:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":endpoint-config/*" + ] + ] + } + ] } ], "Version": "2012-10-17" diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/update-endpoint.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/update-endpoint.test.ts index f628ddbe9ed14..edd90392970b5 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/update-endpoint.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/update-endpoint.test.ts @@ -1,4 +1,5 @@ import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../../lib'; @@ -80,3 +81,65 @@ test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration patt }); }).toThrow(/Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE. Received: WAIT_FOR_TASK_TOKEN/i); }); + +test('PolicyStatement has sufficient permissions', () => { + // WHEN + const props = { + endpointName: 'MyEndpoint', + endpointConfigName: 'MyEndpointConfig', + }; + const task = new tasks.SageMakerUpdateEndpoint(stack, 'SagemakerEndpoint', props); + + const graph = new sfn.StateGraph(task, 'test'); + + // THEN + expect(graph.policyStatements).toEqual( + [ + new iam.PolicyStatement({ + actions: ['sagemaker:updateEndpoint'], + resources: [ + stack.formatArn({ + service: 'sagemaker', + resource: 'endpoint', + resourceName: props.endpointName.toLowerCase(), + }), + stack.formatArn({ + service: 'sagemaker', + resource: 'endpoint-config', + resourceName: props.endpointConfigName.toLowerCase(), + }), + ], + }), + ], + ); + + // WHEN + const props2 = { + endpointName: sfn.JsonPath.stringAt('$.Endpoint.Name'), + endpointConfigName: sfn.JsonPath.stringAt('$.Endpoint.Config'), + }; + const task2 = new tasks.SageMakerUpdateEndpoint(stack, 'SagemakerEndpoint2', props2); + + const graph2 = new sfn.StateGraph(task2, 'test'); + + // THEN + expect(graph2.policyStatements).toEqual( + [ + new iam.PolicyStatement({ + actions: ['sagemaker:updateEndpoint'], + resources: [ + stack.formatArn({ + service: 'sagemaker', + resource: 'endpoint', + resourceName: '*', + }), + stack.formatArn({ + service: 'sagemaker', + resource: 'endpoint-config', + resourceName: '*', + }), + ], + }), + ], + ); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index abcd6fa61a9c9..9485ec700294e 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,200 @@ +# CloudFormation Resource Specification v30.0.0 + +## New Resource Types + +* AWS::EC2::TransitGatewayConnect +* AWS::EKS::Addon +* AWS::EMR::Studio +* AWS::EMR::StudioSessionMapping +* AWS::IAM::OIDCProvider +* AWS::IAM::SAMLProvider +* AWS::IAM::ServerCertificate +* AWS::IAM::VirtualMFADevice +* AWS::SageMaker::Image +* AWS::SageMaker::ImageVersion + +## Attribute Changes + +* AWS::IoTSiteWise::Portal PortalStatus (__deleted__) +* AWS::MediaPackage::Channel HlsIngest (__deleted__) +* AWS::QuickSight::Dashboard Version (__deleted__) +* AWS::QuickSight::Template Version (__deleted__) +* AWS::QuickSight::Theme Version (__deleted__) +* AWS::S3::AccessPoint PolicyStatus (__deleted__) +* AWS::SageMaker::Project ServiceCatalogProvisionedProductDetails (__deleted__) +* AWS::ServiceCatalog::CloudFormationProvisionedProduct Outputs (__deleted__) + +## Property Changes + +* AWS::ECR::PublicRepository Tags (__added__) +* AWS::IVS::Channel RecordingConfigurationArn (__added__) +* AWS::IoTSiteWise::AssetModel AssetModelCompositeModels (__added__) +* AWS::IoTSiteWise::Portal PortalAuthMode (__added__) +* AWS::SageMaker::Model InferenceExecutionConfig (__added__) + +## Property Type Changes + +* AWS::IoTSiteWise::Portal.MonitorErrorDetails (__removed__) +* AWS::IoTSiteWise::Portal.PortalStatus (__removed__) +* AWS::MediaPackage::Channel.HlsIngest (__removed__) +* AWS::MediaPackage::Channel.IngestEndpoint (__removed__) +* AWS::QuickSight::Dashboard.DashboardError (__removed__) +* AWS::QuickSight::Dashboard.DashboardVersion (__removed__) +* AWS::QuickSight::Dashboard.Sheet (__removed__) +* AWS::QuickSight::Template.ColumnGroupColumnSchema (__removed__) +* AWS::QuickSight::Template.ColumnGroupSchema (__removed__) +* AWS::QuickSight::Template.ColumnSchema (__removed__) +* AWS::QuickSight::Template.DataSetConfiguration (__removed__) +* AWS::QuickSight::Template.DataSetSchema (__removed__) +* AWS::QuickSight::Template.Sheet (__removed__) +* AWS::QuickSight::Template.TemplateError (__removed__) +* AWS::QuickSight::Template.TemplateVersion (__removed__) +* AWS::QuickSight::Theme.ThemeError (__removed__) +* AWS::QuickSight::Theme.ThemeVersion (__removed__) +* AWS::IoTSiteWise::AccessPolicy.IamRole (__added__) +* AWS::IoTSiteWise::AccessPolicy.IamUser (__added__) +* AWS::IoTSiteWise::AssetModel.AssetModelCompositeModel (__added__) +* AWS::SageMaker::Model.InferenceExecutionConfig (__added__) +* AWS::ACMPCA::Certificate.Validity Value.PrimitiveType (__changed__) + * Old: Integer + * New: Double +* AWS::CodeBuild::ReportGroup.S3ReportExportConfig BucketOwner (__added__) +* AWS::IoTSiteWise::AccessPolicy.AccessPolicyIdentity IamRole (__added__) +* AWS::IoTSiteWise::AccessPolicy.AccessPolicyIdentity IamUser (__added__) +* AWS::IoTSiteWise::AssetModel.AssetModelProperty DataTypeSpec (__added__) +* AWS::Pinpoint::Campaign.CampaignSmsMessage EntityId (__added__) +* AWS::Pinpoint::Campaign.CampaignSmsMessage TemplateId (__added__) +* AWS::Synthetics::Canary.Code Handler.Required (__changed__) + * Old: false + * New: true +* AWS::Synthetics::Canary.RunConfig TimeoutInSeconds.Required (__changed__) + * Old: true + * New: false + + +# CloudFormation Resource Specification v29.0.0 + +## New Resource Types + +## Attribute Changes + +* AWS::EC2::TransitGatewayMulticastDomainAssociation State (__added__) +* AWS::EC2::TransitGatewayMulticastGroupMember TransitGatewayAttachmentId (__added__) +* AWS::EC2::TransitGatewayMulticastGroupSource TransitGatewayAttachmentId (__added__) +* AWS::IoTWireless::ServiceProfile LoRaWANResponse (__deleted__) +* AWS::IoTWireless::ServiceProfile ChannelMask (__added__) +* AWS::IoTWireless::ServiceProfile DevStatusReqFreq (__added__) +* AWS::IoTWireless::ServiceProfile DlBucketSize (__added__) +* AWS::IoTWireless::ServiceProfile DlRate (__added__) +* AWS::IoTWireless::ServiceProfile DlRatePolicy (__added__) +* AWS::IoTWireless::ServiceProfile DrMax (__added__) +* AWS::IoTWireless::ServiceProfile DrMin (__added__) +* AWS::IoTWireless::ServiceProfile HrAllowed (__added__) +* AWS::IoTWireless::ServiceProfile MinGwDiversity (__added__) +* AWS::IoTWireless::ServiceProfile NwkGeoLoc (__added__) +* AWS::IoTWireless::ServiceProfile PrAllowed (__added__) +* AWS::IoTWireless::ServiceProfile RaAllowed (__added__) +* AWS::IoTWireless::ServiceProfile ReportDevStatusBattery (__added__) +* AWS::IoTWireless::ServiceProfile ReportDevStatusMargin (__added__) +* AWS::IoTWireless::ServiceProfile TargetPer (__added__) +* AWS::IoTWireless::ServiceProfile UlBucketSize (__added__) +* AWS::IoTWireless::ServiceProfile UlRate (__added__) +* AWS::IoTWireless::ServiceProfile UlRatePolicy (__added__) +* AWS::IoTWireless::WirelessDevice ThingArn (__deleted__) +* AWS::IoTWireless::WirelessGateway ThingArn (__deleted__) +* AWS::IoTWireless::WirelessGateway ThingName (__added__) + +## Property Changes + +* AWS::CodeBuild::Project ConcurrentBuildLimit (__added__) +* AWS::DataBrew::Job JobSample (__added__) +* AWS::DynamoDB::Table KinesisStreamSpecification (__added__) +* AWS::EC2::TransitGatewayMulticastDomain TransitGatewayId.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastDomainAssociation State (__deleted__) +* AWS::EC2::TransitGatewayMulticastDomainAssociation SubnetId.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastDomainAssociation TransitGatewayAttachmentId.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastDomainAssociation TransitGatewayMulticastDomainId.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastGroupMember TransitGatewayAttachmentId (__deleted__) +* AWS::EC2::TransitGatewayMulticastGroupMember GroupIpAddress.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastGroupMember NetworkInterfaceId.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastGroupMember TransitGatewayMulticastDomainId.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastGroupSource TransitGatewayAttachmentId (__deleted__) +* AWS::EC2::TransitGatewayMulticastGroupSource GroupIpAddress.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastGroupSource NetworkInterfaceId.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::TransitGatewayMulticastGroupSource TransitGatewayMulticastDomainId.Required (__changed__) + * Old: false + * New: true +* AWS::ElastiCache::GlobalReplicationGroup CacheParameterGroupName (__added__) +* AWS::ElasticLoadBalancingV2::TargetGroup ProtocolVersion (__added__) +* AWS::ImageBuilder::Image ContainerRecipeArn (__added__) +* AWS::ImageBuilder::Image ImageRecipeArn.Required (__changed__) + * Old: true + * New: false +* AWS::ImageBuilder::ImagePipeline ContainerRecipeArn (__added__) +* AWS::ImageBuilder::ImagePipeline ImageRecipeArn.Required (__changed__) + * Old: true + * New: false +* AWS::IoTAnalytics::Dataset LateDataRules (__added__) +* AWS::IoTWireless::WirelessDevice ThingArn (__added__) +* AWS::IoTWireless::WirelessGateway ThingName (__deleted__) +* AWS::IoTWireless::WirelessGateway ThingArn (__added__) +* AWS::StepFunctions::StateMachine Definition (__added__) + +## Property Type Changes + +* AWS::IoTWireless::ServiceProfile.LoRaWANGetServiceProfileInfo (__removed__) +* AWS::DynamoDB::Table.KinesisStreamSpecification (__added__) +* AWS::IoTAnalytics::Dataset.DeltaTimeSessionWindowConfiguration (__added__) +* AWS::IoTAnalytics::Dataset.LateDataRule (__added__) +* AWS::IoTAnalytics::Dataset.LateDataRuleConfiguration (__added__) +* AWS::StepFunctions::StateMachine.Definition (__added__) +* AWS::CodeCommit::Repository.Code BranchName.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ElasticLoadBalancingV2::TargetGroup.Matcher GrpcCode (__added__) +* AWS::FSx::FileSystem.WindowsConfiguration Aliases (__added__) +* AWS::FSx::FileSystem.WindowsConfiguration ThroughputCapacity.Required (__changed__) + * Old: false + * New: true +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile ChannelMask (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile DevStatusReqFreq (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile DlBucketSize (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile DlRate (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile DlRatePolicy (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile DrMax (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile DrMin (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile HrAllowed (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile MinGwDiversity (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile NwkGeoLoc (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile PrAllowed (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile RaAllowed (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile ReportDevStatusBattery (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile ReportDevStatusMargin (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile TargetPer (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile UlBucketSize (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile UlRate (__added__) +* AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile UlRatePolicy (__added__) +* AWS::Pinpoint::Campaign.CampaignSmsMessage OriginationNumber (__added__) + + # CloudFormation Resource Specification v28.0.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index 0b4dfcf095f29..8dd5c17a1b5cd 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -28.0.0 +30.0.0 diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index eca5e7601bdb5..75da81b0c270b 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -402,7 +402,7 @@ }, "Value": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-acmpca-certificate-validity.html#cfn-acmpca-certificate-validity-value", - "PrimitiveType": "Integer", + "PrimitiveType": "Double", "Required": true, "UpdateType": "Immutable" } @@ -11494,6 +11494,12 @@ "Required": true, "UpdateType": "Mutable" }, + "BucketOwner": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-reportgroup-s3reportexportconfig.html#cfn-codebuild-reportgroup-s3reportexportconfig-bucketowner", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "EncryptionDisabled": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-reportgroup-s3reportexportconfig.html#cfn-codebuild-reportgroup-s3reportexportconfig-encryptiondisabled", "PrimitiveType": "Boolean", @@ -11527,7 +11533,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codecommit-repository-code.html#cfn-codecommit-repository-code-branchname", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "S3": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codecommit-repository-code.html#cfn-codecommit-repository-code-s3", @@ -15702,6 +15708,17 @@ } } }, + "AWS::DynamoDB::Table.KinesisStreamSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-kinesisstreamspecification.html", + "Properties": { + "StreamArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-kinesisstreamspecification.html#cfn-dynamodb-kinesisstreamspecification-streamarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::DynamoDB::Table.LocalSecondaryIndex": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-lsi.html", "Properties": { @@ -18808,6 +18825,17 @@ } } }, + "AWS::EC2::TransitGatewayConnect.TransitGatewayConnectOptions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-transitgatewayconnect-transitgatewayconnectoptions.html", + "Properties": { + "Protocol": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-transitgatewayconnect-transitgatewayconnectoptions.html#cfn-ec2-transitgatewayconnect-transitgatewayconnectoptions-protocol", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::EC2::VPNConnection.VpnTunnelOptionsSpecification": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-vpnconnection-vpntunneloptionsspecification.html", "Properties": { @@ -23137,6 +23165,12 @@ "AWS::ElasticLoadBalancingV2::TargetGroup.Matcher": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-targetgroup-matcher.html", "Properties": { + "GrpcCode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-targetgroup-matcher.html#cfn-elasticloadbalancingv2-targetgroup-matcher-grpccode", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "HttpCode": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-elasticloadbalancingv2-targetgroup-matcher.html#cfn-elasticloadbalancingv2-targetgroup-matcher-httpcode", "PrimitiveType": "String", @@ -24122,6 +24156,13 @@ "Required": false, "UpdateType": "Immutable" }, + "Aliases": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration.html#cfn-fsx-filesystem-windowsconfiguration-aliases", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "AutomaticBackupRetentionDays": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration.html#cfn-fsx-filesystem-windowsconfiguration-automaticbackupretentiondays", "PrimitiveType": "Integer", @@ -24161,7 +24202,7 @@ "ThroughputCapacity": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-fsx-filesystem-windowsconfiguration.html#cfn-fsx-filesystem-windowsconfiguration-throughputcapacity", "PrimitiveType": "Integer", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "WeeklyMaintenanceStartTime": { @@ -28797,6 +28838,17 @@ } } }, + "AWS::IoTAnalytics::Dataset.DeltaTimeSessionWindowConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-deltatimesessionwindowconfiguration.html", + "Properties": { + "TimeoutInMinutes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-deltatimesessionwindowconfiguration.html#cfn-iotanalytics-dataset-deltatimesessionwindowconfiguration-timeoutinminutes", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::IoTAnalytics::Dataset.Filter": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-filter.html", "Properties": { @@ -28842,6 +28894,34 @@ } } }, + "AWS::IoTAnalytics::Dataset.LateDataRule": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-latedatarule.html", + "Properties": { + "RuleConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-latedatarule.html#cfn-iotanalytics-dataset-latedatarule-ruleconfiguration", + "Required": true, + "Type": "LateDataRuleConfiguration", + "UpdateType": "Mutable" + }, + "RuleName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-latedatarule.html#cfn-iotanalytics-dataset-latedatarule-rulename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::IoTAnalytics::Dataset.LateDataRuleConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-latedataruleconfiguration.html", + "Properties": { + "DeltaTimeSessionWindowConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-latedataruleconfiguration.html#cfn-iotanalytics-dataset-latedataruleconfiguration-deltatimesessionwindowconfiguration", + "Required": false, + "Type": "DeltaTimeSessionWindowConfiguration", + "UpdateType": "Mutable" + } + } + }, "AWS::IoTAnalytics::Dataset.OutputFileUriValue": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-variable-outputfileurivalue.html", "Properties": { @@ -30106,6 +30186,18 @@ "AWS::IoTSiteWise::AccessPolicy.AccessPolicyIdentity": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-accesspolicy-accesspolicyidentity.html", "Properties": { + "IamRole": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-accesspolicy-accesspolicyidentity.html#cfn-iotsitewise-accesspolicy-accesspolicyidentity-iamrole", + "Required": false, + "Type": "IamRole", + "UpdateType": "Mutable" + }, + "IamUser": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-accesspolicy-accesspolicyidentity.html#cfn-iotsitewise-accesspolicy-accesspolicyidentity-iamuser", + "Required": false, + "Type": "IamUser", + "UpdateType": "Mutable" + }, "User": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-accesspolicy-accesspolicyidentity.html#cfn-iotsitewise-accesspolicy-accesspolicyidentity-user", "Required": false, @@ -30131,6 +30223,28 @@ } } }, + "AWS::IoTSiteWise::AccessPolicy.IamRole": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-accesspolicy-iamrole.html", + "Properties": { + "arn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-accesspolicy-iamrole.html#cfn-iotsitewise-accesspolicy-iamrole-arn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::IoTSiteWise::AccessPolicy.IamUser": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-accesspolicy-iamuser.html", + "Properties": { + "arn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-accesspolicy-iamuser.html#cfn-iotsitewise-accesspolicy-iamuser-arn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::IoTSiteWise::AccessPolicy.Portal": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-accesspolicy-portal.html", "Properties": { @@ -30204,6 +30318,36 @@ } } }, + "AWS::IoTSiteWise::AssetModel.AssetModelCompositeModel": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-assetmodel-assetmodelcompositemodel.html", + "Properties": { + "CompositeModelProperties": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-assetmodel-assetmodelcompositemodel.html#cfn-iotsitewise-assetmodel-assetmodelcompositemodel-compositemodelproperties", + "ItemType": "AssetModelProperty", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-assetmodel-assetmodelcompositemodel.html#cfn-iotsitewise-assetmodel-assetmodelcompositemodel-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-assetmodel-assetmodelcompositemodel.html#cfn-iotsitewise-assetmodel-assetmodelcompositemodel-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-assetmodel-assetmodelcompositemodel.html#cfn-iotsitewise-assetmodel-assetmodelcompositemodel-type", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::IoTSiteWise::AssetModel.AssetModelHierarchy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-assetmodel-assetmodelhierarchy.html", "Properties": { @@ -30236,6 +30380,12 @@ "Required": true, "UpdateType": "Mutable" }, + "DataTypeSpec": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-assetmodel-assetmodelproperty.html#cfn-iotsitewise-assetmodel-assetmodelproperty-datatypespec", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "LogicalId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-assetmodel-assetmodelproperty.html#cfn-iotsitewise-assetmodel-assetmodelproperty-logicalid", "PrimitiveType": "String", @@ -30439,40 +30589,6 @@ } } }, - "AWS::IoTSiteWise::Portal.MonitorErrorDetails": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-portal-monitorerrordetails.html", - "Properties": { - "code": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-portal-monitorerrordetails.html#cfn-iotsitewise-portal-monitorerrordetails-code", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "message": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-portal-monitorerrordetails.html#cfn-iotsitewise-portal-monitorerrordetails-message", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - } - } - }, - "AWS::IoTSiteWise::Portal.PortalStatus": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-portal-portalstatus.html", - "Properties": { - "error": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-portal-portalstatus.html#cfn-iotsitewise-portal-portalstatus-error", - "Required": false, - "Type": "MonitorErrorDetails", - "UpdateType": "Mutable" - }, - "state": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotsitewise-portal-portalstatus.html#cfn-iotsitewise-portal-portalstatus-state", - "PrimitiveType": "String", - "Required": true, - "UpdateType": "Mutable" - } - } - }, "AWS::IoTThingsGraph::FlowTemplate.DefinitionDocument": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotthingsgraph-flowtemplate-definitiondocument.html", "Properties": { @@ -30579,136 +30695,125 @@ } } }, - "AWS::IoTWireless::ServiceProfile.LoRaWANGetServiceProfileInfo": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html", + "AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html", "Properties": { "AddGwMetadata": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-addgwmetadata", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-addgwmetadata", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "ChannelMask": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-channelmask", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-channelmask", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" }, "DevStatusReqFreq": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-devstatusreqfreq", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-devstatusreqfreq", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "DlBucketSize": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-dlbucketsize", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-dlbucketsize", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "DlRate": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-dlrate", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-dlrate", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "DlRatePolicy": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-dlratepolicy", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-dlratepolicy", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" }, "DrMax": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-drmax", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-drmax", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "DrMin": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-drmin", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-drmin", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "HrAllowed": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-hrallowed", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-hrallowed", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "MinGwDiversity": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-mingwdiversity", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-mingwdiversity", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "NwkGeoLoc": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-nwkgeoloc", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-nwkgeoloc", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "PrAllowed": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-prallowed", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-prallowed", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "RaAllowed": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-raallowed", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-raallowed", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "ReportDevStatusBattery": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-reportdevstatusbattery", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-reportdevstatusbattery", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "ReportDevStatusMargin": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-reportdevstatusmargin", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-reportdevstatusmargin", "PrimitiveType": "Boolean", "Required": false, "UpdateType": "Mutable" }, "TargetPer": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-targetper", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-targetper", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "UlBucketSize": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-ulbucketsize", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-ulbucketsize", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "UlRate": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-ulrate", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-ulrate", "PrimitiveType": "Integer", "Required": false, "UpdateType": "Mutable" }, "UlRatePolicy": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawangetserviceprofileinfo.html#cfn-iotwireless-serviceprofile-lorawangetserviceprofileinfo-ulratepolicy", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-ulratepolicy", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" } } }, - "AWS::IoTWireless::ServiceProfile.LoRaWANServiceProfile": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html", - "Properties": { - "AddGwMetadata": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-serviceprofile-lorawanserviceprofile.html#cfn-iotwireless-serviceprofile-lorawanserviceprofile-addgwmetadata", - "PrimitiveType": "Boolean", - "Required": false, - "UpdateType": "Mutable" - } - } - }, "AWS::IoTWireless::WirelessDevice.AbpV10x": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iotwireless-wirelessdevice-abpv10x.html", "Properties": { @@ -40648,47 +40753,6 @@ } } }, - "AWS::MediaPackage::Channel.HlsIngest": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-mediapackage-channel-hlsingest.html", - "Properties": { - "ingestEndpoints": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-mediapackage-channel-hlsingest.html#cfn-mediapackage-channel-hlsingest-ingestendpoints", - "ItemType": "IngestEndpoint", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - } - } - }, - "AWS::MediaPackage::Channel.IngestEndpoint": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-mediapackage-channel-ingestendpoint.html", - "Properties": { - "Id": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-mediapackage-channel-ingestendpoint.html#cfn-mediapackage-channel-ingestendpoint-id", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "Password": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-mediapackage-channel-ingestendpoint.html#cfn-mediapackage-channel-ingestendpoint-password", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "Url": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-mediapackage-channel-ingestendpoint.html#cfn-mediapackage-channel-ingestendpoint-url", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "Username": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-mediapackage-channel-ingestendpoint.html#cfn-mediapackage-channel-ingestendpoint-username", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - } - } - }, "AWS::MediaPackage::OriginEndpoint.Authorization": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-mediapackage-originendpoint-authorization.html", "Properties": { @@ -42919,17 +42983,35 @@ "Required": false, "UpdateType": "Mutable" }, + "EntityId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-pinpoint-campaign-campaignsmsmessage.html#cfn-pinpoint-campaign-campaignsmsmessage-entityid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "MessageType": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-pinpoint-campaign-campaignsmsmessage.html#cfn-pinpoint-campaign-campaignsmsmessage-messagetype", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" }, + "OriginationNumber": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-pinpoint-campaign-campaignsmsmessage.html#cfn-pinpoint-campaign-campaignsmsmessage-originationnumber", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "SenderId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-pinpoint-campaign-campaignsmsmessage.html#cfn-pinpoint-campaign-campaignsmsmessage-senderid", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" + }, + "TemplateId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-pinpoint-campaign-campaignsmsmessage.html#cfn-pinpoint-campaign-campaignsmsmessage-templateid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -44094,23 +44176,6 @@ } } }, - "AWS::QuickSight::Dashboard.DashboardError": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboarderror.html", - "Properties": { - "Message": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboarderror.html#cfn-quicksight-dashboard-dashboarderror-message", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "Type": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboarderror.html#cfn-quicksight-dashboard-dashboarderror-type", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - } - } - }, "AWS::QuickSight::Dashboard.DashboardPublishOptions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboardpublishoptions.html", "Properties": { @@ -44163,74 +44228,6 @@ } } }, - "AWS::QuickSight::Dashboard.DashboardVersion": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboardversion.html", - "Properties": { - "Arn": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboardversion.html#cfn-quicksight-dashboard-dashboardversion-arn", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "CreatedTime": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboardversion.html#cfn-quicksight-dashboard-dashboardversion-createdtime", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "DataSetArns": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboardversion.html#cfn-quicksight-dashboard-dashboardversion-datasetarns", - "PrimitiveItemType": "String", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - }, - "Description": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboardversion.html#cfn-quicksight-dashboard-dashboardversion-description", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "Errors": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboardversion.html#cfn-quicksight-dashboard-dashboardversion-errors", - "ItemType": "DashboardError", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - }, - "Sheets": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboardversion.html#cfn-quicksight-dashboard-dashboardversion-sheets", - "ItemType": "Sheet", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - }, - "SourceEntityArn": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboardversion.html#cfn-quicksight-dashboard-dashboardversion-sourceentityarn", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "Status": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboardversion.html#cfn-quicksight-dashboard-dashboardversion-status", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "ThemeArn": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboardversion.html#cfn-quicksight-dashboard-dashboardversion-themearn", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "VersionNumber": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-dashboardversion.html#cfn-quicksight-dashboard-dashboardversion-versionnumber", - "PrimitiveType": "Double", - "Required": false, - "UpdateType": "Mutable" - } - } - }, "AWS::QuickSight::Dashboard.DataSetReference": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-datasetreference.html", "Properties": { @@ -44364,23 +44361,6 @@ } } }, - "AWS::QuickSight::Dashboard.Sheet": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-sheet.html", - "Properties": { - "Name": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-sheet.html#cfn-quicksight-dashboard-sheet-name", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "SheetId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-sheet.html#cfn-quicksight-dashboard-sheet-sheetid", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - } - } - }, "AWS::QuickSight::Dashboard.SheetControlsOption": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-dashboard-sheetcontrolsoption.html", "Properties": { @@ -44410,82 +44390,6 @@ } } }, - "AWS::QuickSight::Template.ColumnGroupColumnSchema": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-columngroupcolumnschema.html", - "Properties": { - "Name": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-columngroupcolumnschema.html#cfn-quicksight-template-columngroupcolumnschema-name", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - } - } - }, - "AWS::QuickSight::Template.ColumnGroupSchema": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-columngroupschema.html", - "Properties": { - "ColumnGroupColumnSchemaList": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-columngroupschema.html#cfn-quicksight-template-columngroupschema-columngroupcolumnschemalist", - "ItemType": "ColumnGroupColumnSchema", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - }, - "Name": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-columngroupschema.html#cfn-quicksight-template-columngroupschema-name", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - } - } - }, - "AWS::QuickSight::Template.ColumnSchema": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-columnschema.html", - "Properties": { - "DataType": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-columnschema.html#cfn-quicksight-template-columnschema-datatype", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "GeographicRole": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-columnschema.html#cfn-quicksight-template-columnschema-geographicrole", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "Name": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-columnschema.html#cfn-quicksight-template-columnschema-name", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - } - } - }, - "AWS::QuickSight::Template.DataSetConfiguration": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-datasetconfiguration.html", - "Properties": { - "ColumnGroupSchemaList": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-datasetconfiguration.html#cfn-quicksight-template-datasetconfiguration-columngroupschemalist", - "ItemType": "ColumnGroupSchema", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - }, - "DataSetSchema": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-datasetconfiguration.html#cfn-quicksight-template-datasetconfiguration-datasetschema", - "Required": false, - "Type": "DataSetSchema", - "UpdateType": "Mutable" - }, - "Placeholder": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-datasetconfiguration.html#cfn-quicksight-template-datasetconfiguration-placeholder", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - } - } - }, "AWS::QuickSight::Template.DataSetReference": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-datasetreference.html", "Properties": { @@ -44503,18 +44407,6 @@ } } }, - "AWS::QuickSight::Template.DataSetSchema": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-datasetschema.html", - "Properties": { - "ColumnSchemaList": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-datasetschema.html#cfn-quicksight-template-datasetschema-columnschemalist", - "ItemType": "ColumnSchema", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - } - } - }, "AWS::QuickSight::Template.ResourcePermission": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-resourcepermission.html", "Properties": { @@ -44533,40 +44425,6 @@ } } }, - "AWS::QuickSight::Template.Sheet": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-sheet.html", - "Properties": { - "Name": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-sheet.html#cfn-quicksight-template-sheet-name", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "SheetId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-sheet.html#cfn-quicksight-template-sheet-sheetid", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - } - } - }, - "AWS::QuickSight::Template.TemplateError": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-templateerror.html", - "Properties": { - "Message": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-templateerror.html#cfn-quicksight-template-templateerror-message", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "Type": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-templateerror.html#cfn-quicksight-template-templateerror-type", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - } - } - }, "AWS::QuickSight::Template.TemplateSourceAnalysis": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-templatesourceanalysis.html", "Properties": { @@ -44613,68 +44471,6 @@ } } }, - "AWS::QuickSight::Template.TemplateVersion": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-templateversion.html", - "Properties": { - "CreatedTime": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-templateversion.html#cfn-quicksight-template-templateversion-createdtime", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "DataSetConfigurations": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-templateversion.html#cfn-quicksight-template-templateversion-datasetconfigurations", - "ItemType": "DataSetConfiguration", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - }, - "Description": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-templateversion.html#cfn-quicksight-template-templateversion-description", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "Errors": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-templateversion.html#cfn-quicksight-template-templateversion-errors", - "ItemType": "TemplateError", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - }, - "Sheets": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-templateversion.html#cfn-quicksight-template-templateversion-sheets", - "ItemType": "Sheet", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - }, - "SourceEntityArn": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-templateversion.html#cfn-quicksight-template-templateversion-sourceentityarn", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "Status": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-templateversion.html#cfn-quicksight-template-templateversion-status", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "ThemeArn": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-templateversion.html#cfn-quicksight-template-templateversion-themearn", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "VersionNumber": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-templateversion.html#cfn-quicksight-template-templateversion-versionnumber", - "PrimitiveType": "Double", - "Required": false, - "UpdateType": "Mutable" - } - } - }, "AWS::QuickSight::Theme.BorderStyle": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-theme-borderstyle.html", "Properties": { @@ -44808,77 +44604,6 @@ } } }, - "AWS::QuickSight::Theme.ThemeError": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-theme-themeerror.html", - "Properties": { - "Message": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-theme-themeerror.html#cfn-quicksight-theme-themeerror-message", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "Type": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-theme-themeerror.html#cfn-quicksight-theme-themeerror-type", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - } - } - }, - "AWS::QuickSight::Theme.ThemeVersion": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-theme-themeversion.html", - "Properties": { - "Arn": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-theme-themeversion.html#cfn-quicksight-theme-themeversion-arn", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "BaseThemeId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-theme-themeversion.html#cfn-quicksight-theme-themeversion-basethemeid", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "Configuration": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-theme-themeversion.html#cfn-quicksight-theme-themeversion-configuration", - "Required": false, - "Type": "ThemeConfiguration", - "UpdateType": "Mutable" - }, - "CreatedTime": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-theme-themeversion.html#cfn-quicksight-theme-themeversion-createdtime", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "Description": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-theme-themeversion.html#cfn-quicksight-theme-themeversion-description", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "Errors": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-theme-themeversion.html#cfn-quicksight-theme-themeversion-errors", - "ItemType": "ThemeError", - "Required": false, - "Type": "List", - "UpdateType": "Mutable" - }, - "Status": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-theme-themeversion.html#cfn-quicksight-theme-themeversion-status", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, - "VersionNumber": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-theme-themeversion.html#cfn-quicksight-theme-themeversion-versionnumber", - "PrimitiveType": "Double", - "Required": false, - "UpdateType": "Mutable" - } - } - }, "AWS::QuickSight::Theme.TileLayoutStyle": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-theme-tilelayoutstyle.html", "Properties": { @@ -49025,6 +48750,17 @@ } } }, + "AWS::SageMaker::Model.InferenceExecutionConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-model-inferenceexecutionconfig.html", + "Properties": { + "Mode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-model-inferenceexecutionconfig.html#cfn-sagemaker-model-inferenceexecutionconfig-mode", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::SageMaker::Model.MultiModelConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-model-containerdefinition-multimodelconfig.html", "Properties": { @@ -50896,6 +50632,9 @@ } } }, + "AWS::StepFunctions::StateMachine.Definition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-definition.html" + }, "AWS::StepFunctions::StateMachine.LogDestination": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-logdestination.html", "Properties": { @@ -50988,7 +50727,7 @@ "Handler": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-synthetics-canary-code.html#cfn-synthetics-canary-code-handler", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "S3Bucket": { @@ -51042,7 +50781,7 @@ "TimeoutInSeconds": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-synthetics-canary-runconfig.html#cfn-synthetics-canary-runconfig-timeoutinseconds", "PrimitiveType": "Integer", - "Required": true, + "Required": false, "UpdateType": "Mutable" } } @@ -53317,7 +53056,7 @@ } } }, - "ResourceSpecificationVersion": "28.0.0", + "ResourceSpecificationVersion": "30.0.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -59664,6 +59403,12 @@ "Type": "ProjectCache", "UpdateType": "Mutable" }, + "ConcurrentBuildLimit": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codebuild-project.html#cfn-codebuild-project-concurrentbuildlimit", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, "Description": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codebuild-project.html#cfn-codebuild-project-description", "PrimitiveType": "String", @@ -62183,6 +61928,12 @@ "Required": false, "UpdateType": "Mutable" }, + "JobSample": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-databrew-job.html#cfn-databrew-job-jobsample", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" + }, "LogSubscription": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-databrew-job.html#cfn-databrew-job-logsubscription", "PrimitiveType": "String", @@ -63358,6 +63109,12 @@ "Type": "List", "UpdateType": "Immutable" }, + "KinesisStreamSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html#cfn-dynamodb-table-kinesisstreamspecification", + "Required": false, + "Type": "KinesisStreamSpecification", + "UpdateType": "Mutable" + }, "LocalSecondaryIndexes": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html#cfn-dynamodb-table-lsi", "DuplicatesAllowed": true, @@ -65563,6 +65320,44 @@ } } }, + "AWS::EC2::TransitGatewayConnect": { + "Attributes": { + "CreationTime": { + "PrimitiveType": "String" + }, + "State": { + "PrimitiveType": "String" + }, + "TransitGatewayAttachmentId": { + "PrimitiveType": "String" + }, + "TransitGatewayId": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewayconnect.html", + "Properties": { + "Options": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewayconnect.html#cfn-ec2-transitgatewayconnect-options", + "Required": true, + "Type": "TransitGatewayConnectOptions", + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewayconnect.html#cfn-ec2-transitgatewayconnect-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "TransportTransitGatewayAttachmentId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewayconnect.html#cfn-ec2-transitgatewayconnect-transporttransitgatewayattachmentid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::EC2::TransitGatewayMulticastDomain": { "Attributes": { "CreationTime": { @@ -65596,7 +65391,7 @@ "TransitGatewayId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastdomain.html#cfn-ec2-transitgatewaymulticastdomain-transitgatewayid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" } } @@ -65608,32 +65403,29 @@ }, "ResourceType": { "PrimitiveType": "String" + }, + "State": { + "PrimitiveType": "String" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastdomainassociation.html", "Properties": { - "State": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastdomainassociation.html#cfn-ec2-transitgatewaymulticastdomainassociation-state", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, "SubnetId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastdomainassociation.html#cfn-ec2-transitgatewaymulticastdomainassociation-subnetid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, "TransitGatewayAttachmentId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastdomainassociation.html#cfn-ec2-transitgatewaymulticastdomainassociation-transitgatewayattachmentid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, "TransitGatewayMulticastDomainId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastdomainassociation.html#cfn-ec2-transitgatewaymulticastdomainassociation-transitgatewaymulticastdomainid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" } } @@ -65660,6 +65452,9 @@ }, "SubnetId": { "PrimitiveType": "String" + }, + "TransitGatewayAttachmentId": { + "PrimitiveType": "String" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupmember.html", @@ -65667,25 +65462,19 @@ "GroupIpAddress": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupmember.html#cfn-ec2-transitgatewaymulticastgroupmember-groupipaddress", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, "NetworkInterfaceId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupmember.html#cfn-ec2-transitgatewaymulticastgroupmember-networkinterfaceid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, - "TransitGatewayAttachmentId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupmember.html#cfn-ec2-transitgatewaymulticastgroupmember-transitgatewayattachmentid", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, "TransitGatewayMulticastDomainId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupmember.html#cfn-ec2-transitgatewaymulticastgroupmember-transitgatewaymulticastdomainid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" } } @@ -65712,6 +65501,9 @@ }, "SubnetId": { "PrimitiveType": "String" + }, + "TransitGatewayAttachmentId": { + "PrimitiveType": "String" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupsource.html", @@ -65719,25 +65511,19 @@ "GroupIpAddress": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupsource.html#cfn-ec2-transitgatewaymulticastgroupsource-groupipaddress", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, "NetworkInterfaceId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupsource.html#cfn-ec2-transitgatewaymulticastgroupsource-networkinterfaceid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" }, - "TransitGatewayAttachmentId": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupsource.html#cfn-ec2-transitgatewaymulticastgroupsource-transitgatewayattachmentid", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, "TransitGatewayMulticastDomainId": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-transitgatewaymulticastgroupsource.html#cfn-ec2-transitgatewaymulticastgroupsource-transitgatewaymulticastdomainid", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" } } @@ -66369,6 +66155,14 @@ "PrimitiveType": "Json", "Required": false, "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecr-publicrepository.html#cfn-ecr-publicrepository-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" } } }, @@ -67020,6 +66814,54 @@ } } }, + "AWS::EKS::Addon": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-addon.html", + "Properties": { + "AddonName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-addon.html#cfn-eks-addon-addonname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "AddonVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-addon.html#cfn-eks-addon-addonversion", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ClusterName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-addon.html#cfn-eks-addon-clustername", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "ResolveConflicts": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-addon.html#cfn-eks-addon-resolveconflicts", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ServiceAccountRoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-addon.html#cfn-eks-addon-serviceaccountrolearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-addon.html#cfn-eks-addon-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::EKS::Cluster": { "Attributes": { "Arn": { @@ -67557,6 +67399,120 @@ } } }, + "AWS::EMR::Studio": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "StudioId": { + "PrimitiveType": "String" + }, + "Url": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studio.html", + "Properties": { + "AuthMode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studio.html#cfn-emr-studio-authmode", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "DefaultS3Location": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studio.html#cfn-emr-studio-defaults3location", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studio.html#cfn-emr-studio-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "EngineSecurityGroupId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studio.html#cfn-emr-studio-enginesecuritygroupid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studio.html#cfn-emr-studio-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "ServiceRole": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studio.html#cfn-emr-studio-servicerole", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "SubnetIds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studio.html#cfn-emr-studio-subnetids", + "PrimitiveItemType": "String", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studio.html#cfn-emr-studio-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "UserRole": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studio.html#cfn-emr-studio-userrole", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "VpcId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studio.html#cfn-emr-studio-vpcid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "WorkspaceSecurityGroupId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studio.html#cfn-emr-studio-workspacesecuritygroupid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::EMR::StudioSessionMapping": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studiosessionmapping.html", + "Properties": { + "IdentityName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studiosessionmapping.html#cfn-emr-studiosessionmapping-identityname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "IdentityType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studiosessionmapping.html#cfn-emr-studiosessionmapping-identitytype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "SessionPolicyArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studiosessionmapping.html#cfn-emr-studiosessionmapping-sessionpolicyarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "StudioId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-emr-studiosessionmapping.html#cfn-emr-studiosessionmapping-studioid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::EMRContainers::VirtualCluster": { "Attributes": { "Arn": { @@ -67768,6 +67724,12 @@ "Required": false, "UpdateType": "Mutable" }, + "CacheParameterGroupName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticache-globalreplicationgroup.html#cfn-elasticache-globalreplicationgroup-cacheparametergroupname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "EngineVersion": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticache-globalreplicationgroup.html#cfn-elasticache-globalreplicationgroup-engineversion", "PrimitiveType": "String", @@ -68825,6 +68787,12 @@ "Required": false, "UpdateType": "Immutable" }, + "ProtocolVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-targetgroup.html#cfn-elasticloadbalancingv2-targetgroup-protocolversion", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-targetgroup.html#cfn-elasticloadbalancingv2-targetgroup-tags", "DuplicatesAllowed": true, @@ -71718,6 +71686,44 @@ } } }, + "AWS::IAM::OIDCProvider": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-oidcprovider.html", + "Properties": { + "ClientIdList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-oidcprovider.html#cfn-iam-oidcprovider-clientidlist", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-oidcprovider.html#cfn-iam-oidcprovider-tags", + "DuplicatesAllowed": true, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "ThumbprintList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-oidcprovider.html#cfn-iam-oidcprovider-thumbprintlist", + "PrimitiveItemType": "String", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "Url": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-oidcprovider.html#cfn-iam-oidcprovider-url", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::IAM::Policy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-policy.html", "Properties": { @@ -71832,6 +71838,84 @@ } } }, + "AWS::IAM::SAMLProvider": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-samlprovider.html", + "Properties": { + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-samlprovider.html#cfn-iam-samlprovider-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "SamlMetadataDocument": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-samlprovider.html#cfn-iam-samlprovider-samlmetadatadocument", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-samlprovider.html#cfn-iam-samlprovider-tags", + "DuplicatesAllowed": true, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::IAM::ServerCertificate": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-servercertificate.html", + "Properties": { + "CertificateBody": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-servercertificate.html#cfn-iam-servercertificate-certificatebody", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "CertificateChain": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-servercertificate.html#cfn-iam-servercertificate-certificatechain", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Path": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-servercertificate.html#cfn-iam-servercertificate-path", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "PrivateKey": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-servercertificate.html#cfn-iam-servercertificate-privatekey", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "ServerCertificateName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-servercertificate.html#cfn-iam-servercertificate-servercertificatename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-servercertificate.html#cfn-iam-servercertificate-tags", + "DuplicatesAllowed": true, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::IAM::ServiceLinkedRole": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-servicelinkedrole.html", "Properties": { @@ -71940,6 +72024,44 @@ } } }, + "AWS::IAM::VirtualMFADevice": { + "Attributes": { + "SerialNumber": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-virtualmfadevice.html", + "Properties": { + "Path": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-virtualmfadevice.html#cfn-iam-virtualmfadevice-path", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-virtualmfadevice.html#cfn-iam-virtualmfadevice-tags", + "DuplicatesAllowed": true, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Users": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-virtualmfadevice.html#cfn-iam-virtualmfadevice-users", + "DuplicatesAllowed": true, + "PrimitiveItemType": "String", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "VirtualMfaDeviceName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-virtualmfadevice.html#cfn-iam-virtualmfadevice-virtualmfadevicename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::IVS::Channel": { "Attributes": { "Arn": { @@ -71972,6 +72094,12 @@ "Required": false, "UpdateType": "Mutable" }, + "RecordingConfigurationArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ivs-channel.html#cfn-ivs-channel-recordingconfigurationarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ivs-channel.html#cfn-ivs-channel-tags", "DuplicatesAllowed": false, @@ -72272,6 +72400,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html", "Properties": { + "ContainerRecipeArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-containerrecipearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "DistributionConfigurationArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-distributionconfigurationarn", "PrimitiveType": "String", @@ -72287,7 +72421,7 @@ "ImageRecipeArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-imagerecipearn", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Immutable" }, "ImageTestsConfiguration": { @@ -72319,6 +72453,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html", "Properties": { + "ContainerRecipeArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-containerrecipearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Description": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-description", "PrimitiveType": "String", @@ -72340,7 +72480,7 @@ "ImageRecipeArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-imagerecipearn", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "ImageTestsConfiguration": { @@ -73103,6 +73243,13 @@ "Required": false, "UpdateType": "Immutable" }, + "LateDataRules": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-latedatarules", + "ItemType": "LateDataRule", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "RetentionPeriod": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotanalytics-dataset.html#cfn-iotanalytics-dataset-retentionperiod", "Required": false, @@ -73359,6 +73506,13 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotsitewise-assetmodel.html", "Properties": { + "AssetModelCompositeModels": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotsitewise-assetmodel.html#cfn-iotsitewise-assetmodel-assetmodelcompositemodels", + "ItemType": "AssetModelCompositeModel", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "AssetModelDescription": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotsitewise-assetmodel.html#cfn-iotsitewise-assetmodel-assetmodeldescription", "PrimitiveType": "String", @@ -73490,13 +73644,16 @@ }, "PortalStartUrl": { "PrimitiveType": "String" - }, - "PortalStatus": { - "Type": "PortalStatus" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotsitewise-portal.html", "Properties": { + "PortalAuthMode": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotsitewise-portal.html#cfn-iotsitewise-portal-portalauthmode", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "PortalContactEmail": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotsitewise-portal.html#cfn-iotsitewise-portal-portalcontactemail", "PrimitiveType": "String", @@ -73673,11 +73830,62 @@ "Arn": { "PrimitiveType": "String" }, + "ChannelMask": { + "PrimitiveType": "String" + }, + "DevStatusReqFreq": { + "PrimitiveType": "Integer" + }, + "DlBucketSize": { + "PrimitiveType": "Integer" + }, + "DlRate": { + "PrimitiveType": "Integer" + }, + "DlRatePolicy": { + "PrimitiveType": "String" + }, + "DrMax": { + "PrimitiveType": "Integer" + }, + "DrMin": { + "PrimitiveType": "Integer" + }, + "HrAllowed": { + "PrimitiveType": "Boolean" + }, "Id": { "PrimitiveType": "String" }, - "LoRaWANResponse": { - "Type": "LoRaWANGetServiceProfileInfo" + "MinGwDiversity": { + "PrimitiveType": "Integer" + }, + "NwkGeoLoc": { + "PrimitiveType": "Boolean" + }, + "PrAllowed": { + "PrimitiveType": "Boolean" + }, + "RaAllowed": { + "PrimitiveType": "Boolean" + }, + "ReportDevStatusBattery": { + "PrimitiveType": "Boolean" + }, + "ReportDevStatusMargin": { + "PrimitiveType": "Boolean" + }, + "TargetPer": { + "PrimitiveType": "Integer" + }, + "UlBucketSize": { + "PrimitiveType": "Integer" + }, + "UlRate": { + "PrimitiveType": "Integer" + }, + "UlRatePolicy": { + "PrimitiveType": "String" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-serviceprofile.html", @@ -73712,9 +73920,6 @@ "Id": { "PrimitiveType": "String" }, - "ThingArn": { - "PrimitiveType": "String" - }, "ThingName": { "PrimitiveType": "String" } @@ -73759,6 +73964,12 @@ "Type": "List", "UpdateType": "Mutable" }, + "ThingArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-wirelessdevice.html#cfn-iotwireless-wirelessdevice-thingarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Type": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-wirelessdevice.html#cfn-iotwireless-wirelessdevice-type", "PrimitiveType": "String", @@ -73775,7 +73986,7 @@ "Id": { "PrimitiveType": "String" }, - "ThingArn": { + "ThingName": { "PrimitiveType": "String" } }, @@ -73813,8 +74024,8 @@ "Type": "List", "UpdateType": "Mutable" }, - "ThingName": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-wirelessgateway.html#cfn-iotwireless-wirelessgateway-thingname", + "ThingArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iotwireless-wirelessgateway.html#cfn-iotwireless-wirelessgateway-thingarn", "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" @@ -76388,9 +76599,6 @@ "Attributes": { "Arn": { "PrimitiveType": "String" - }, - "HlsIngest": { - "Type": "HlsIngest" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-mediapackage-channel.html", @@ -79317,9 +79525,6 @@ }, "LastUpdatedTime": { "PrimitiveType": "String" - }, - "Version": { - "Type": "DashboardVersion" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-quicksight-dashboard.html", @@ -79398,9 +79603,6 @@ }, "LastUpdatedTime": { "PrimitiveType": "String" - }, - "Version": { - "Type": "TemplateVersion" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-quicksight-template.html", @@ -79464,9 +79666,6 @@ }, "Type": { "PrimitiveType": "String" - }, - "Version": { - "Type": "ThemeVersion" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-quicksight-theme.html", @@ -81555,9 +81754,6 @@ "Attributes": { "NetworkOrigin": { "PrimitiveType": "String" - }, - "PolicyStatus": { - "PrimitiveType": "Json" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3-accesspoint.html", @@ -83248,6 +83444,78 @@ } } }, + "AWS::SageMaker::Image": { + "Attributes": { + "ImageArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-image.html", + "Properties": { + "ImageDescription": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-image.html#cfn-sagemaker-image-imagedescription", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ImageDisplayName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-image.html#cfn-sagemaker-image-imagedisplayname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ImageName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-image.html#cfn-sagemaker-image-imagename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "ImageRoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-image.html#cfn-sagemaker-image-imagerolearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-image.html#cfn-sagemaker-image-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::SageMaker::ImageVersion": { + "Attributes": { + "ContainerImage": { + "PrimitiveType": "String" + }, + "ImageArn": { + "PrimitiveType": "String" + }, + "ImageVersionArn": { + "PrimitiveType": "String" + }, + "Version": { + "PrimitiveType": "Integer" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-imageversion.html", + "Properties": { + "BaseImage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-imageversion.html#cfn-sagemaker-imageversion-baseimage", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "ImageName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-imageversion.html#cfn-sagemaker-imageversion-imagename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::SageMaker::Model": { "Attributes": { "ModelName": { @@ -83275,6 +83543,12 @@ "Required": true, "UpdateType": "Immutable" }, + "InferenceExecutionConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-model.html#cfn-sagemaker-model-inferenceexecutionconfig", + "Required": false, + "Type": "InferenceExecutionConfig", + "UpdateType": "Immutable" + }, "ModelName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-model.html#cfn-sagemaker-model-modelname", "PrimitiveType": "String", @@ -83808,9 +84082,6 @@ }, "ProjectStatus": { "PrimitiveType": "String" - }, - "ServiceCatalogProvisionedProductDetails": { - "PrimitiveType": "Json" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-sagemaker-project.html", @@ -84166,10 +84437,6 @@ "CloudformationStackArn": { "PrimitiveType": "String" }, - "Outputs": { - "PrimitiveItemType": "String", - "Type": "Map" - }, "ProvisionedProductId": { "PrimitiveType": "String" }, @@ -85120,6 +85387,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html", "Properties": { + "Definition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-definition", + "Required": false, + "Type": "Definition", + "UpdateType": "Mutable" + }, "DefinitionS3Location": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-definitions3location", "Required": false, diff --git a/packages/@aws-cdk/cfnspec/spec-source/cfn-lint/StatefulResources/000.json b/packages/@aws-cdk/cfnspec/spec-source/cfn-lint/StatefulResources/000.json index 25c5e11c890ff..36a17855e22b7 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/cfn-lint/StatefulResources/000.json +++ b/packages/@aws-cdk/cfnspec/spec-source/cfn-lint/StatefulResources/000.json @@ -27,4 +27,3 @@ } } } - diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index 2a1705ab876fd..bcbb1c7193e68 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -25,7 +25,7 @@ "colors": "^1.4.0", "diff": "^5.0.0", "fast-deep-equal": "^3.1.3", - "string-width": "^4.2.0", + "string-width": "^4.2.2", "table": "^6.0.7" }, "devDependencies": { @@ -36,7 +36,7 @@ "fast-check": "^2.13.0", "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.5.1" + "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 457f11834e88a..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.1" + "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/README.md b/packages/@aws-cdk/core/README.md index 0e0b38943a803..bb519f066af55 100644 --- a/packages/@aws-cdk/core/README.md +++ b/packages/@aws-cdk/core/README.md @@ -427,7 +427,8 @@ stack-unique identifier and returns the service token: ```ts const serviceToken = CustomResourceProvider.getOrCreate(this, 'Custom::MyCustomResourceType', { codeDirectory: `${__dirname}/my-handler`, - runtime: CustomResourceProviderRuntime.NODEJS_12, // currently the only supported runtime + runtime: CustomResourceProviderRuntime.NODEJS_12_X, + description: "Lambda function created by the custom resource provider", }); new CustomResource(this, 'MyResource', { @@ -521,7 +522,7 @@ export class Sum extends Construct { const resourceType = 'Custom::Sum'; const serviceToken = CustomResourceProvider.getOrCreate(this, resourceType, { codeDirectory: `${__dirname}/sum-handler`, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, }); const resource = new CustomResource(this, 'Resource', { @@ -551,7 +552,7 @@ built-in singleton method: ```ts const provider = CustomResourceProvider.getOrCreateProvider(this, 'Custom::MyCustomResourceType', { codeDirectory: `${__dirname}/my-handler`, - runtime: CustomResourceProviderRuntime.NODEJS_12, // currently the only supported runtime + runtime: CustomResourceProviderRuntime.NODEJS_12_X, }); const roleArn = provider.roleArn; diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index 66c65e3d14864..6a34bd9b4b1ac 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -5,8 +5,8 @@ import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import * as fs from 'fs-extra'; import * as minimatch from 'minimatch'; -import { AssetHashType, AssetOptions } from './assets'; -import { BundlingOptions } from './bundling'; +import { AssetHashType, AssetOptions, FileAssetPackaging } from './assets'; +import { BundlingOptions, BundlingOutput } from './bundling'; import { FileSystem, FingerprintOptions } from './fs'; import { Names } from './names'; import { Cache } from './private/cache'; @@ -17,6 +17,8 @@ import { Stage } from './stage'; // eslint-disable-next-line import { Construct as CoreConstruct } from './construct-compat'; +const ARCHIVE_EXTENSIONS = ['.zip', '.jar']; + /** * A previously staged asset */ @@ -30,6 +32,16 @@ interface StagedAsset { * The hash we used previously */ readonly assetHash: string; + + /** + * The packaging of the asset + */ + readonly packaging: FileAssetPackaging, + + /** + * Whether this asset is an archive + */ + readonly isArchive: boolean; } /** @@ -124,6 +136,16 @@ export class AssetStaging extends CoreConstruct { */ public readonly assetHash: string; + /** + * How this asset should be packaged. + */ + public readonly packaging: FileAssetPackaging; + + /** + * Whether this asset is an archive (zip or jar). + */ + public readonly isArchive: boolean; + private readonly fingerprintOptions: FingerprintOptions; private readonly hashType: AssetHashType; @@ -138,12 +160,20 @@ export class AssetStaging extends CoreConstruct { private readonly cacheKey: string; + private readonly sourceStats: fs.Stats; + constructor(scope: Construct, id: string, props: AssetStagingProps) { super(scope, id); this.sourcePath = path.resolve(props.sourcePath); this.fingerprintOptions = props; + if (!fs.existsSync(this.sourcePath)) { + throw new Error(`Cannot find asset at ${this.sourcePath}`); + } + + this.sourceStats = fs.statSync(this.sourcePath); + const outdir = Stage.of(this)?.assetOutdir; if (!outdir) { throw new Error('unable to determine cloud assembly asset output directory. Assets must be defined indirectly within a "Stage" or an "App" scope'); @@ -192,6 +222,8 @@ export class AssetStaging extends CoreConstruct { this.stagedPath = staged.stagedPath; this.absoluteStagedPath = staged.stagedPath; this.assetHash = staged.assetHash; + this.packaging = staged.packaging; + this.isArchive = staged.isArchive; } /** @@ -248,8 +280,18 @@ export class AssetStaging extends CoreConstruct { ? this.sourcePath : path.resolve(this.assetOutdir, renderAssetFilename(assetHash, path.extname(this.sourcePath))); + if (!this.sourceStats.isDirectory() && !this.sourceStats.isFile()) { + throw new Error(`Asset ${this.sourcePath} is expected to be either a directory or a regular file`); + } + this.stageAsset(this.sourcePath, stagedPath, 'copy'); - return { assetHash, stagedPath }; + + return { + assetHash, + stagedPath, + packaging: this.sourceStats.isDirectory() ? FileAssetPackaging.ZIP_DIRECTORY : FileAssetPackaging.FILE, + isArchive: this.sourceStats.isDirectory() || ARCHIVE_EXTENSIONS.includes(path.extname(this.sourcePath).toLowerCase()), + }; } /** @@ -258,6 +300,10 @@ export class AssetStaging extends CoreConstruct { * Optionally skip, in which case we pretend we did something but we don't really. */ private stageByBundling(bundling: BundlingOptions, skip: boolean): StagedAsset { + if (!this.sourceStats.isDirectory()) { + throw new Error(`Asset ${this.sourcePath} is expected to be a directory when bundling`); + } + if (skip) { // We should have bundled, but didn't to save time. Still pretend to have a hash. // If the asset uses OUTPUT or BUNDLE, we use a CUSTOM hash to avoid fingerprinting @@ -270,6 +316,8 @@ export class AssetStaging extends CoreConstruct { return { assetHash: this.calculateHash(hashType, bundling), stagedPath: this.sourcePath, + packaging: FileAssetPackaging.ZIP_DIRECTORY, + isArchive: true, }; } @@ -281,12 +329,21 @@ export class AssetStaging extends CoreConstruct { const bundleDir = this.determineBundleDir(this.assetOutdir, assetHash); this.bundle(bundling, bundleDir); - // Calculate assetHash afterwards if we still must - assetHash = assetHash ?? this.calculateHash(this.hashType, bundling, bundleDir); - const stagedPath = path.resolve(this.assetOutdir, renderAssetFilename(assetHash)); + // Check bundling output content and determine if we will need to archive + const bundlingOutputType = bundling.outputType ?? BundlingOutput.AUTO_DISCOVER; + const bundledAsset = determineBundledAsset(bundleDir, bundlingOutputType); - this.stageAsset(bundleDir, stagedPath, 'move'); - return { assetHash, stagedPath }; + // Calculate assetHash afterwards if we still must + assetHash = assetHash ?? this.calculateHash(this.hashType, bundling, bundledAsset.path); + const stagedPath = path.resolve(this.assetOutdir, renderAssetFilename(assetHash, bundledAsset.extension)); + + this.stageAsset(bundledAsset.path, stagedPath, 'move'); + return { + assetHash, + stagedPath, + packaging: bundledAsset.packaging, + isArchive: true, // bundling always produces an archive + }; } /** @@ -320,10 +377,9 @@ export class AssetStaging extends CoreConstruct { } // Copy file/directory to staging directory - const stat = fs.statSync(sourcePath); - if (stat.isFile()) { + if (this.sourceStats.isFile()) { fs.copyFileSync(sourcePath, targetPath); - } else if (stat.isDirectory()) { + } else if (this.sourceStats.isDirectory()) { fs.mkdirSync(targetPath); FileSystem.copyDirectory(sourcePath, targetPath, this.fingerprintOptions); } else { @@ -502,3 +558,57 @@ function sortObject(object: { [key: string]: any }): { [key: string]: any } { } return ret; } + +/** + * Returns the single archive file of a directory or undefined + */ +function singleArchiveFile(directory: string): string | undefined { + if (!fs.existsSync(directory)) { + throw new Error(`Directory ${directory} does not exist.`); + } + + if (!fs.statSync(directory).isDirectory()) { + throw new Error(`${directory} is not a directory.`); + } + + const content = fs.readdirSync(directory); + if (content.length === 1) { + const file = path.join(directory, content[0]); + const extension = path.extname(content[0]).toLowerCase(); + if (fs.statSync(file).isFile() && ARCHIVE_EXTENSIONS.includes(extension)) { + return file; + } + } + + return undefined; +} + +interface BundledAsset { + path: string, + packaging: FileAssetPackaging, + extension?: string +} + +/** + * Returns the bundled asset to use based on the content of the bundle directory + * and the type of output. + */ +function determineBundledAsset(bundleDir: string, outputType: BundlingOutput): BundledAsset { + const archiveFile = singleArchiveFile(bundleDir); + + // auto-discover means that if there is an archive file, we take it as the + // bundle, otherwise, we will archive here. + if (outputType === BundlingOutput.AUTO_DISCOVER) { + outputType = archiveFile ? BundlingOutput.ARCHIVED : BundlingOutput.NOT_ARCHIVED; + } + + switch (outputType) { + case BundlingOutput.NOT_ARCHIVED: + return { path: bundleDir, packaging: FileAssetPackaging.ZIP_DIRECTORY }; + case BundlingOutput.ARCHIVED: + if (!archiveFile) { + throw new Error('Bundling output directory is expected to include only a single .zip or .jar file when `output` is set to `ARCHIVED`'); + } + return { path: archiveFile, packaging: FileAssetPackaging.FILE, extension: path.extname(archiveFile) }; + } +} diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index b1247fd913ea0..e3c1458aa0ab9 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -1,5 +1,6 @@ import { spawnSync, SpawnSyncOptions } from 'child_process'; import * as crypto from 'crypto'; +import { isAbsolute, join } from 'path'; import { FileSystem } from './fs'; /** @@ -79,6 +80,41 @@ export interface BundlingOptions { * @experimental */ readonly local?: ILocalBundling; + + /** + * The type of output that this bundling operation is producing. + * + * @default BundlingOutput.AUTO_DISCOVER + * + * @experimental + */ + readonly outputType?: BundlingOutput; +} + +/** + * The type of output that a bundling operation is producing. + * + * @experimental + */ +export enum BundlingOutput { + /** + * The bundling output directory includes a single .zip or .jar file which + * will be used as the final bundle. If the output directory does not + * include exactly a single archive, bundling will fail. + */ + ARCHIVED = 'archived', + + /** + * The bundling output directory contains one or more files which will be + * archived and uploaded as a .zip file to S3. + */ + NOT_ARCHIVED = 'not-archived', + + /** + * If the bundling output directory contains a single archive file (zip or jar) + * it will be used as the bundle output as-is. Otherwise all the files in the bundling output directory will be zipped. + */ + AUTO_DISCOVER = 'auto-discover', } /** @@ -100,6 +136,8 @@ export interface ILocalBundling { /** * A Docker image used for asset bundling + * + * @deprecated use DockerImage */ export class BundlingDockerImage { /** @@ -116,20 +154,24 @@ export class BundlingDockerImage { * * @param path The path to the directory containing the Docker file * @param options Docker build options + * + * @deprecated use DockerImage.fromBuild() */ public static fromAsset(path: string, options: DockerBuildOptions = {}) { const buildArgs = options.buildArgs || {}; + if (options.file && isAbsolute(options.file)) { + throw new Error(`"file" must be relative to the docker build directory. Got ${options.file}`); + } + // Image tag derived from path and build options - const tagHash = crypto.createHash('sha256').update(JSON.stringify({ - path, - ...options, - })).digest('hex'); + const input = JSON.stringify({ path, ...options }); + const tagHash = crypto.createHash('sha256').update(input).digest('hex'); const tag = `cdk-${tagHash}`; const dockerArgs: string[] = [ 'build', '-t', tag, - ...(options.file ? ['-f', options.file] : []), + ...(options.file ? ['-f', join(path, options.file)] : []), ...flatten(Object.entries(buildArgs).map(([k, v]) => ['--build-arg', `${k}=${v}`])), path, ]; @@ -146,7 +188,7 @@ export class BundlingDockerImage { } /** @param image The Docker image */ - private constructor(public readonly image: string, private readonly _imageHash?: string) {} + protected constructor(public readonly image: string, private readonly _imageHash?: string) {} /** * Provides a stable representation of this image for JSON serialization. @@ -194,10 +236,16 @@ export class BundlingDockerImage { } /** - * Copies a file or directory out of the Docker image to the local filesystem + * Copies a file or directory out of the Docker image to the local filesystem. + * + * If `outputPath` is omitted the destination path is a temporary directory. + * + * @param imagePath the path in the Docker image + * @param outputPath the destination path for the copy operation + * @returns the destination path */ - public cp(imagePath: string, outputPath: string) { - const { stdout } = dockerExec(['create', this.image]); + public cp(imagePath: string, outputPath?: string): string { + const { stdout } = dockerExec(['create', this.image], {}); // Empty options to avoid stdout redirect here const match = stdout.toString().match(/([0-9a-f]{16,})/); if (!match) { throw new Error('Failed to extract container ID from Docker create output'); @@ -205,16 +253,33 @@ export class BundlingDockerImage { const containerId = match[1]; const containerPath = `${containerId}:${imagePath}`; + const destPath = outputPath ?? FileSystem.mkdtemp('cdk-docker-cp-'); try { - dockerExec(['cp', containerPath, outputPath]); + dockerExec(['cp', containerPath, destPath]); + return destPath; } catch (err) { - throw new Error(`Failed to copy files from ${containerPath} to ${outputPath}: ${err}`); + throw new Error(`Failed to copy files from ${containerPath} to ${destPath}: ${err}`); } finally { dockerExec(['rm', '-v', containerId]); } } } +/** + * A Docker image + */ +export class DockerImage extends BundlingDockerImage { + /** + * Builds a Docker image + * + * @param path The path to the directory containing the Docker file + * @param options Docker build options + */ + public static fromBuild(path: string, options: DockerBuildOptions = {}) { + return BundlingDockerImage.fromAsset(path, options); + } +} + /** * A Docker volume */ @@ -315,9 +380,9 @@ export interface DockerBuildOptions { readonly buildArgs?: { [key: string]: string }; /** - * Name of the Dockerfile + * Name of the Dockerfile, must relative to the docker build path. * - * @default - The Dockerfile immediately within the build context path + * @default `Dockerfile` */ readonly file?: string; } diff --git a/packages/@aws-cdk/core/lib/cfn-json.ts b/packages/@aws-cdk/core/lib/cfn-json.ts index 0e5801ed8ade9..df6bc40ef9f9e 100644 --- a/packages/@aws-cdk/core/lib/cfn-json.ts +++ b/packages/@aws-cdk/core/lib/cfn-json.ts @@ -43,7 +43,7 @@ export class CfnJson extends CoreConstruct implements IResolvable { * Normally there is no need to use this property since `CfnJson` is an * IResolvable, so it can be simply used as a value. */ - private readonly value: Reference; + public readonly value: Reference; private readonly jsonString: string; 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/custom-resource-provider/custom-resource-provider.ts b/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts index f7905fc51447b..c7f3776339907 100644 --- a/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts +++ b/packages/@aws-cdk/core/lib/custom-resource-provider/custom-resource-provider.ts @@ -67,6 +67,13 @@ export interface CustomResourceProviderProps { * @default - No environment variables. */ readonly environment?: { [key: string]: string }; + + /** + * A description of the function. + * + * @default - No description. + */ + readonly description?: string; } /** @@ -77,8 +84,20 @@ export interface CustomResourceProviderProps { export enum CustomResourceProviderRuntime { /** * Node.js 12.x + * + * @deprecated Use {@link NODEJS_12_X} + */ + NODEJS_12 = 'nodejs12.x', + + /** + * Node.js 12.x + */ + NODEJS_12_X = 'nodejs12.x', + + /** + * Node.js 14.x */ - NODEJS_12 = 'nodejs12' + NODEJS_14_X = 'nodejs14.x', } /** @@ -203,8 +222,9 @@ export class CustomResourceProvider extends CoreConstruct { MemorySize: memory.toMebibytes(), Handler: `${ENTRYPOINT_FILENAME}.handler`, Role: role.getAtt('Arn'), - Runtime: 'nodejs12.x', + Runtime: props.runtime, Environment: this.renderEnvironmentVariables(props.environment), + Description: props.description ?? undefined, }, }); diff --git a/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts b/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts index dae7253720041..8200165fbfe34 100644 --- a/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts +++ b/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts @@ -7,7 +7,7 @@ import { CustomResourceProvider, CustomResourceProviderRuntime } from '../custom export class CfnUtilsProvider extends Construct { public static getOrCreate(scope: Construct) { return CustomResourceProvider.getOrCreate(scope, 'AWSCDKCfnUtilsProvider', { - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, codeDirectory: `${__dirname}/cfn-utils-provider`, }); } diff --git a/packages/@aws-cdk/core/test/archive/archive.zip b/packages/@aws-cdk/core/test/archive/archive.zip new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/@aws-cdk/core/test/bundling.test.ts b/packages/@aws-cdk/core/test/bundling.test.ts index 258860d65585c..99548030c011a 100644 --- a/packages/@aws-cdk/core/test/bundling.test.ts +++ b/packages/@aws-cdk/core/test/bundling.test.ts @@ -3,7 +3,7 @@ import * as crypto from 'crypto'; import * as path from 'path'; import { nodeunitShim, Test } from 'nodeunit-shim'; import * as sinon from 'sinon'; -import { BundlingDockerImage, FileSystem } from '../lib'; +import { BundlingDockerImage, DockerImage, FileSystem } from '../lib'; nodeunitShim({ 'tearDown'(callback: any) { @@ -161,12 +161,14 @@ nodeunitShim({ signal: null, }); - BundlingDockerImage.fromAsset(path.join(__dirname, 'fs/fixtures/test1'), { + const imagePath = path.join(__dirname, 'fs/fixtures/test1'); + BundlingDockerImage.fromAsset(imagePath, { file: 'my-dockerfile', }); test.ok(spawnSyncStub.calledOnce); - test.ok(/-f my-dockerfile/.test(spawnSyncStub.firstCall.args[1]?.join(' ') ?? '')); + const expected = path.join(imagePath, 'my-dockerfile'); + test.ok(new RegExp(`-f ${expected}`).test(spawnSyncStub.firstCall.args[1]?.join(' ') ?? '')); test.done(); }, @@ -263,4 +265,25 @@ nodeunitShim({ test.ok(spawnSyncStub.calledWith(sinon.match.any, ['rm', '-v', containerId])); test.done(); }, + + 'cp utility copies to a temp dir of outputPath is omitted'(test: Test) { + // GIVEN + const containerId = '1234567890abcdef1234567890abcdef'; + sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from(`${containerId}\n`), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + // WHEN + const tempPath = DockerImage.fromRegistry('alpine').cp('/foo/bar'); + + // THEN + test.ok(/cdk-docker-cp-/.test(tempPath)); + + test.done(); + }, }); diff --git a/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts b/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts index b6c1e608e2f59..5fc12ecc17c2f 100644 --- a/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts +++ b/packages/@aws-cdk/core/test/custom-resource-provider/custom-resource-provider.test.ts @@ -14,7 +14,7 @@ nodeunitShim({ // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, }); // THEN @@ -149,7 +149,7 @@ nodeunitShim({ // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, }); // THEN -- no exception @@ -167,7 +167,7 @@ nodeunitShim({ // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, policyStatements: [ { statement1: 123 }, { statement2: { foo: 111 } }, @@ -187,16 +187,17 @@ nodeunitShim({ test.done(); }, - 'memorySize and timeout'(test: Test) { + 'memorySize, timeout and description'(test: Test) { // GIVEN const stack = new Stack(); // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, memorySize: Size.gibibytes(2), timeout: Duration.minutes(5), + description: 'veni vidi vici', }); // THEN @@ -204,6 +205,7 @@ nodeunitShim({ const lambda = template.Resources.CustomMyResourceTypeCustomResourceProviderHandler29FBDD2A; test.deepEqual(lambda.Properties.MemorySize, 2048); test.deepEqual(lambda.Properties.Timeout, 300); + test.deepEqual(lambda.Properties.Description, 'veni vidi vici'); test.done(); }, @@ -214,7 +216,7 @@ nodeunitShim({ // WHEN CustomResourceProvider.getOrCreate(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, environment: { B: 'b', A: 'a', @@ -240,7 +242,7 @@ nodeunitShim({ // WHEN const cr = CustomResourceProvider.getOrCreateProvider(stack, 'Custom:MyResourceType', { codeDirectory: TEST_HANDLER, - runtime: CustomResourceProviderRuntime.NODEJS_12, + runtime: CustomResourceProviderRuntime.NODEJS_12_X, }); // THEN diff --git a/packages/@aws-cdk/core/test/docker-stub.sh b/packages/@aws-cdk/core/test/docker-stub.sh index fe48e93d4a207..94f806f69a120 100755 --- a/packages/@aws-cdk/core/test/docker-stub.sh +++ b/packages/@aws-cdk/core/test/docker-stub.sh @@ -24,5 +24,18 @@ if echo "$@" | grep "DOCKER_STUB_SUCCESS"; then exit 0 fi -echo "Docker mock only supports one of the following commands: DOCKER_STUB_SUCCESS_NO_OUTPUT,DOCKER_STUB_FAIL,DOCKER_STUB_SUCCESS" +if echo "$@" | grep "DOCKER_STUB_MULTIPLE_FILES"; then + outdir=$(echo "$@" | xargs -n1 | grep "/asset-output" | head -n1 | cut -d":" -f1) + touch ${outdir}/test1.txt + touch ${outdir}/test2.txt + exit 0 +fi + +if echo "$@" | grep "DOCKER_STUB_SINGLE_ARCHIVE"; then + outdir=$(echo "$@" | xargs -n1 | grep "/asset-output" | head -n1 | cut -d":" -f1) + touch ${outdir}/test.zip + exit 0 +fi + +echo "Docker mock only supports one of the following commands: DOCKER_STUB_SUCCESS_NO_OUTPUT,DOCKER_STUB_FAIL,DOCKER_STUB_SUCCESS,DOCKER_STUB_MULTIPLE_FILES,DOCKER_SINGLE_ARCHIVE" exit 1 diff --git a/packages/@aws-cdk/core/test/staging.test.ts b/packages/@aws-cdk/core/test/staging.test.ts index 347c5fcea3b63..ee87780a0957e 100644 --- a/packages/@aws-cdk/core/test/staging.test.ts +++ b/packages/@aws-cdk/core/test/staging.test.ts @@ -1,10 +1,11 @@ import * as os from 'os'; import * as path from 'path'; +import { FileAssetPackaging } from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; import { nodeunitShim, Test } from 'nodeunit-shim'; import * as sinon from 'sinon'; -import { App, AssetHashType, AssetStaging, BundlingDockerImage, BundlingOptions, FileSystem, Stack, Stage } from '../lib'; +import { App, AssetHashType, AssetStaging, BundlingDockerImage, BundlingOptions, BundlingOutput, FileSystem, Stack, Stage } from '../lib'; const STUB_INPUT_FILE = '/tmp/docker-stub.input'; const STUB_INPUT_CONCAT_FILE = '/tmp/docker-stub.input.concat'; @@ -12,7 +13,9 @@ const STUB_INPUT_CONCAT_FILE = '/tmp/docker-stub.input.concat'; enum DockerStubCommand { SUCCESS = 'DOCKER_STUB_SUCCESS', FAIL = 'DOCKER_STUB_FAIL', - SUCCESS_NO_OUTPUT = 'DOCKER_STUB_SUCCESS_NO_OUTPUT' + SUCCESS_NO_OUTPUT = 'DOCKER_STUB_SUCCESS_NO_OUTPUT', + MULTIPLE_FILES = 'DOCKER_STUB_MULTIPLE_FILES', + SINGLE_ARCHIVE = 'DOCKER_STUB_SINGLE_ARCHIVE', } const FIXTURE_TEST1_DIR = path.join(__dirname, 'fs', 'fixtures', 'test1'); @@ -50,6 +53,84 @@ nodeunitShim({ test.deepEqual(staging.sourcePath, sourcePath); test.deepEqual(path.basename(staging.stagedPath), 'asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'); test.deepEqual(path.basename(staging.relativeStagedPath(stack)), 'asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00'); + test.deepEqual(staging.packaging, FileAssetPackaging.ZIP_DIRECTORY); + test.deepEqual(staging.isArchive, true); + test.done(); + }, + + 'staging of an archive file correctly sets packaging and isArchive'(test: Test) { + // GIVEN + const stack = new Stack(); + const sourcePath = path.join(__dirname, 'archive', 'archive.zip'); + + // WHEN + const staging = new AssetStaging(stack, 's1', { sourcePath }); + + test.deepEqual(staging.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging.isArchive, true); + test.done(); + }, + + 'asset packaging type is correct when staging is skipped because of memory cache'(test: Test) { + // GIVEN + const stack = new Stack(); + const sourcePath = path.join(__dirname, 'archive', 'archive.zip'); + + // WHEN + const staging1 = new AssetStaging(stack, 's1', { sourcePath }); + const staging2 = new AssetStaging(stack, 's2', { sourcePath }); + + test.deepEqual(staging1.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging1.isArchive, true); + test.deepEqual(staging2.packaging, staging1.packaging); + test.deepEqual(staging2.isArchive, staging1.isArchive); + test.done(); + }, + + 'asset packaging type is correct when staging is skipped because of disk cache'(test: Test) { + // GIVEN + const TEST_OUTDIR = path.join(__dirname, 'cdk.out'); + if (fs.existsSync(TEST_OUTDIR)) { + fs.removeSync(TEST_OUTDIR); + } + + const sourcePath = path.join(__dirname, 'archive', 'archive.zip'); + + const app1 = new App({ outdir: TEST_OUTDIR }); + const stack1 = new Stack(app1, 'Stack'); + + const app2 = new App({ outdir: TEST_OUTDIR }); // same OUTDIR + const stack2 = new Stack(app2, 'stack'); + + // WHEN + const staging1 = new AssetStaging(stack1, 'Asset', { sourcePath }); + + // Now clear asset hash cache to show that during the second staging + // even though the asset is already available on disk it will correctly + // be considered as a FileAssetPackaging.FILE. + AssetStaging.clearAssetHashCache(); + + const staging2 = new AssetStaging(stack2, 'Asset', { sourcePath }); + + // THEN + test.deepEqual(staging1.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging1.isArchive, true); + test.deepEqual(staging2.packaging, staging1.packaging); + test.deepEqual(staging2.isArchive, staging1.isArchive); + + test.done(); + }, + + 'staging of a non-archive file correctly sets packaging and isArchive'(test: Test) { + // GIVEN + const stack = new Stack(); + const sourcePath = __filename; + + // WHEN + const staging = new AssetStaging(stack, 's1', { sourcePath }); + + test.deepEqual(staging.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging.isArchive, false); test.done(); }, @@ -785,6 +866,89 @@ nodeunitShim({ ); test.equal(asset.assetHash, '33cbf2cae5432438e0f046bc45ba8c3cef7b6afcf47b59d1c183775c1918fb1f'); // hash of MyStack/Asset + test.done(); + }, + + 'bundling that produces a single archive file is autodiscovered'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + const staging = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SINGLE_ARCHIVE], + }, + }); + + // THEN + const assembly = app.synth(); + test.deepEqual(fs.readdirSync(assembly.directory), [ + 'asset.f43148c61174f444925231b5849b468f21e93b5d1469cd07c53625ffd039ef48', // this is the bundle dir but it's empty + 'asset.f43148c61174f444925231b5849b468f21e93b5d1469cd07c53625ffd039ef48.zip', + 'cdk.out', + 'manifest.json', + 'stack.template.json', + 'tree.json', + ]); + test.equal(fs.readdirSync(path.join(assembly.directory, 'asset.f43148c61174f444925231b5849b468f21e93b5d1469cd07c53625ffd039ef48')).length, 0); // empty bundle dir + test.deepEqual(staging.packaging, FileAssetPackaging.FILE); + test.deepEqual(staging.isArchive, true); + + test.done(); + }, + + 'bundling that produces a single archive file with NOT_ARCHIVED'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + const staging = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.SINGLE_ARCHIVE], + outputType: BundlingOutput.NOT_ARCHIVED, + }, + }); + + // THEN + const assembly = app.synth(); + test.deepEqual(fs.readdirSync(assembly.directory), [ + 'asset.86ec07746e1d859290cfd8b9c648e581555649c75f51f741f11e22cab6775abc', + 'cdk.out', + 'manifest.json', + 'stack.template.json', + 'tree.json', + ]); + test.deepEqual(staging.packaging, FileAssetPackaging.ZIP_DIRECTORY); + test.deepEqual(staging.isArchive, true); + + test.done(); + }, + + 'throws with ARCHIVED and bundling that does not produce a single archive file'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + test.throws(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: [DockerStubCommand.MULTIPLE_FILES], + outputType: BundlingOutput.ARCHIVED, + }, + }), /Bundling output directory is expected to include only a single .zip or .jar file when `output` is set to `ARCHIVED`/); + + test.done(); }, }); diff --git a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts index 3a92062b779cb..bc86cea1c9d43 100644 --- a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts +++ b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts @@ -5,7 +5,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { PHYSICAL_RESOURCE_ID_REFERENCE, flatten } from './runtime'; +import { PHYSICAL_RESOURCE_ID_REFERENCE } from './runtime'; // 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 @@ -14,13 +14,23 @@ import { Construct as CoreConstruct } from '@aws-cdk/core'; /** * Reference to the physical resource id that can be passed to the AWS operation as a parameter. */ -export class PhysicalResourceIdReference { +export class PhysicalResourceIdReference implements cdk.IResolvable { + public readonly creationStack: string[] = cdk.captureStackTrace(); + /** * toJSON serialization to replace `PhysicalResourceIdReference` with a magic string. */ public toJSON() { return PHYSICAL_RESOURCE_ID_REFERENCE; } + + public resolve(_: cdk.IResolveContext): any { + return PHYSICAL_RESOURCE_ID_REFERENCE; + } + + public toString(): string { + return PHYSICAL_RESOURCE_ID_REFERENCE; + } } /** @@ -316,13 +326,8 @@ export class AwsCustomResource extends CoreConstruct implements iam.IGrantable { } } - if (props.onCreate?.parameters) { - const flattenedOnCreateParams = flatten(JSON.parse(JSON.stringify(props.onCreate.parameters))); - for (const param in flattenedOnCreateParams) { - if (flattenedOnCreateParams[param] === PHYSICAL_RESOURCE_ID_REFERENCE) { - throw new Error('`PhysicalResourceIdReference` must not be specified in `onCreate` parameters.'); - } - } + if (includesPhysicalResourceIdRef(props.onCreate?.parameters)) { + throw new Error('`PhysicalResourceIdReference` must not be specified in `onCreate` parameters.'); } this.props = props; @@ -371,9 +376,9 @@ export class AwsCustomResource extends CoreConstruct implements iam.IGrantable { serviceToken: provider.functionArn, pascalCaseProperties: true, properties: { - create: create && encodeBooleans(create), - update: props.onUpdate && encodeBooleans(props.onUpdate), - delete: props.onDelete && encodeBooleans(props.onDelete), + create: create && this.encodeJson(create), + update: props.onUpdate && this.encodeJson(props.onUpdate), + delete: props.onDelete && this.encodeJson(props.onDelete), installLatestAwsSdk: props.installLatestAwsSdk ?? true, }, }); @@ -418,6 +423,9 @@ export class AwsCustomResource extends CoreConstruct implements iam.IGrantable { return this.customResource.getAttString(dataPath); } + private encodeJson(obj: any) { + return cdk.Lazy.uncachedString({ produce: () => cdk.Stack.of(this).toJsonString(obj) }); + } } /** @@ -439,6 +447,30 @@ let getAwsSdkMetadata = (() => { }; })(); +/** + * Returns true if `obj` includes a `PhysicalResourceIdReference` in one of the + * values. + * @param obj Any object. + */ +function includesPhysicalResourceIdRef(obj: any | undefined) { + if (obj === undefined) { + return false; + } + + let foundRef = false; + + // we use JSON.stringify as a way to traverse all values in the object. + JSON.stringify(obj, (_, v) => { + if (v === PHYSICAL_RESOURCE_ID_REFERENCE) { + foundRef = true; + } + + return v; + }); + + return foundRef; +} + /** * Transform SDK service/action to IAM action using metadata from aws-sdk module. * Example: CloudWatchLogs with putRetentionPolicy => logs:PutRetentionPolicy @@ -452,19 +484,3 @@ function awsSdkToIamAction(service: string, action: string): string { const iamAction = action.charAt(0).toUpperCase() + action.slice(1); return `${iamService}:${iamAction}`; } - -/** - * Encodes booleans as special strings - */ -function encodeBooleans(object: object) { - return JSON.parse(JSON.stringify(object), (_k, v) => { - switch (v) { - case true: - return 'TRUE:BOOLEAN'; - case false: - return 'FALSE:BOOLEAN'; - default: - return v; - } - }); -} diff --git a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts index 9f90c8f629f74..5fafd6db214e2 100644 --- a/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts +++ b/packages/@aws-cdk/custom-resources/lib/aws-custom-resource/runtime/index.ts @@ -13,7 +13,7 @@ export const PHYSICAL_RESOURCE_ID_REFERENCE = 'PHYSICAL:RESOURCEID:'; * @param object the object to be flattened * @returns a flat object with path as keys */ -export function flatten(object: object): { [key: string]: string } { +export function flatten(object: object): { [key: string]: any } { return Object.assign( {}, ...function _flatten(child: any, path: string[] = []): any { @@ -29,15 +29,11 @@ export function flatten(object: object): { [key: string]: string } { } /** - * Decodes encoded special values (booleans and physicalResourceId) + * Decodes encoded special values (physicalResourceId) */ function decodeSpecialValues(object: object, physicalResourceId: string) { return JSON.parse(JSON.stringify(object), (_k, v) => { switch (v) { - case 'TRUE:BOOLEAN': - return true; - case 'FALSE:BOOLEAN': - return false; case PHYSICAL_RESOURCE_ID_REFERENCE: return physicalResourceId; default: @@ -96,6 +92,9 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent console.log(JSON.stringify(event)); console.log('AWS SDK VERSION: ' + AWS.VERSION); + event.ResourceProperties.Create = decodeCall(event.ResourceProperties.Create); + event.ResourceProperties.Update = decodeCall(event.ResourceProperties.Update); + event.ResourceProperties.Delete = decodeCall(event.ResourceProperties.Delete); // Default physical resource id let physicalResourceId: string; switch (event.RequestType) { @@ -185,3 +184,8 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent }); } } + +function decodeCall(call: string | undefined) { + if (!call) { return undefined; } + return JSON.parse(call); +} \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index aed86e63bdb0f..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.7", + "nock": "^13.0.10", "pkglint": "0.0.0", "sinon": "^9.2.4" }, diff --git a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts index 0a7e5ec220c28..66f4fbb5dfc08 100644 --- a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts +++ b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource-provider.test.ts @@ -60,14 +60,14 @@ test('create event with physical resource id path', async () => { RequestType: 'Create', ResourceProperties: { ServiceToken: 'token', - Create: { + Create: JSON.stringify({ service: 'S3', action: 'listObjects', parameters: { Bucket: 'my-bucket', }, physicalResourceId: PhysicalResourceId.fromResponse('Contents.1.ETag'), - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -98,7 +98,7 @@ test('update event with physical resource id', async () => { OldResourceProperties: {}, ResourceProperties: { ServiceToken: 'token', - Update: { + Update: JSON.stringify({ service: 'SNS', action: 'publish', parameters: { @@ -106,7 +106,7 @@ test('update event with physical resource id', async () => { TopicArn: 'topicarn', }, physicalResourceId: PhysicalResourceId.of('topicarn'), - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -131,14 +131,14 @@ test('delete event', async () => { PhysicalResourceId: 'physicalResourceId', ResourceProperties: { ServiceToken: 'token', - Create: { + Create: JSON.stringify({ service: 'S3', action: 'listObjects', parameters: { Bucket: 'my-bucket', }, physicalResourceId: PhysicalResourceId.fromResponse('Contents.1.ETag'), - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -166,13 +166,13 @@ test('delete event with Delete call and no physical resource id in call', async PhysicalResourceId: 'physicalResourceId', ResourceProperties: { ServiceToken: 'token', - Delete: { + Delete: JSON.stringify({ service: 'SSM', action: 'deleteParameter', parameters: { Name: 'my-param', }, - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -200,13 +200,13 @@ test('create event with Delete call only', async () => { RequestType: 'Create', ResourceProperties: { ServiceToken: 'token', - Delete: { + Delete: JSON.stringify({ service: 'SSM', action: 'deleteParameter', parameters: { Name: 'my-param', }, - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -234,7 +234,7 @@ test('catch errors', async () => { RequestType: 'Create', ResourceProperties: { ServiceToken: 'token', - Create: { + Create: JSON.stringify({ service: 'S3', action: 'listObjects', parameters: { @@ -242,7 +242,7 @@ test('catch errors', async () => { }, physicalResourceId: PhysicalResourceId.of('physicalResourceId'), ignoreErrorCodesMatching: 'NoSuchBucket', - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -257,68 +257,6 @@ test('catch errors', async () => { expect(request.isDone()).toBeTruthy(); }); -test('decodes booleans', async () => { - const putItemFake = sinon.fake.resolves({}); - - AWS.mock('DynamoDB', 'putItem', putItemFake); - - const event: AWSLambda.CloudFormationCustomResourceCreateEvent = { - ...eventCommon, - RequestType: 'Create', - ResourceProperties: { - ServiceToken: 'token', - Create: { - service: 'DynamoDB', - action: 'putItem', - parameters: { - TableName: 'table', - Item: { - True: { - BOOL: 'TRUE:BOOLEAN', - }, - TrueString: { - S: 'true', - }, - False: { - BOOL: 'FALSE:BOOLEAN', - }, - FalseString: { - S: 'false', - }, - }, - }, - physicalResourceId: PhysicalResourceId.of('put-item'), - } as AwsSdkCall, - }, - }; - - const request = createRequest(body => - body.Status === 'SUCCESS', - ); - - await handler(event, {} as AWSLambda.Context); - - sinon.assert.calledWith(putItemFake, { - TableName: 'table', - Item: { - True: { - BOOL: true, - }, - TrueString: { - S: 'true', - }, - False: { - BOOL: false, - }, - FalseString: { - S: 'false', - }, - }, - }); - - expect(request.isDone()).toBeTruthy(); -}); - test('restrict output path', async () => { const listObjectsFake = sinon.fake.resolves({ Contents: [ @@ -340,7 +278,7 @@ test('restrict output path', async () => { RequestType: 'Create', ResourceProperties: { ServiceToken: 'token', - Create: { + Create: JSON.stringify({ service: 'S3', action: 'listObjects', parameters: { @@ -348,7 +286,7 @@ test('restrict output path', async () => { }, physicalResourceId: PhysicalResourceId.of('id'), outputPath: 'Contents.0', - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -374,7 +312,7 @@ test('can specify apiVersion and region', async () => { RequestType: 'Create', ResourceProperties: { ServiceToken: 'token', - Create: { + Create: JSON.stringify({ service: 'SNS', action: 'publish', parameters: { @@ -384,7 +322,7 @@ test('can specify apiVersion and region', async () => { apiVersion: '2010-03-31', region: 'eu-west-1', physicalResourceId: PhysicalResourceId.of('id'), - } as AwsSdkCall, + } as AwsSdkCall), }, }; @@ -454,7 +392,7 @@ test('installs the latest SDK', async () => { RequestType: 'Create', ResourceProperties: { ServiceToken: 'token', - Create: { + Create: JSON.stringify({ service: 'SNS', action: 'publish', parameters: { @@ -462,7 +400,7 @@ test('installs the latest SDK', async () => { TopicArn: 'topic', }, physicalResourceId: PhysicalResourceId.of('id'), - } as AwsSdkCall, + } as AwsSdkCall), InstallLatestAwsSdk: 'true', }, }; diff --git a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts index 8bf6a667f5c83..086ef301ea41c 100644 --- a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts +++ b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts @@ -34,7 +34,7 @@ test('aws sdk js custom resource with onCreate and onDelete', () => { // THEN expect(stack).toHaveResource('Custom::LogRetentionPolicy', { - 'Create': { + 'Create': JSON.stringify({ 'service': 'CloudWatchLogs', 'action': 'putRetentionPolicy', 'parameters': { @@ -44,14 +44,14 @@ test('aws sdk js custom resource with onCreate and onDelete', () => { 'physicalResourceId': { 'id': 'loggroup', }, - }, - 'Delete': { + }), + 'Delete': JSON.stringify({ 'service': 'CloudWatchLogs', 'action': 'deleteRetentionPolicy', 'parameters': { 'logGroupName': '/aws/lambda/loggroup', }, - }, + }), 'InstallLatestAwsSdk': true, }); @@ -96,7 +96,7 @@ test('onCreate defaults to onUpdate', () => { // THEN expect(stack).toHaveResource('Custom::S3PutObject', { - 'Create': { + 'Create': JSON.stringify({ 'service': 's3', 'action': 'putObject', 'parameters': { @@ -107,8 +107,8 @@ test('onCreate defaults to onUpdate', () => { 'physicalResourceId': { 'responsePath': 'ETag', }, - }, - 'Update': { + }), + 'Update': JSON.stringify({ 'service': 's3', 'action': 'putObject', 'parameters': { @@ -119,7 +119,7 @@ test('onCreate defaults to onUpdate', () => { 'physicalResourceId': { 'responsePath': 'ETag', }, - }, + }), }); }); @@ -185,7 +185,7 @@ test('fails when no physical resource method is specified', () => { })).toThrow(/`physicalResourceId`/); }); -test('encodes booleans', () => { +test('booleans are encoded in the stringified parameters object', () => { // GIVEN const stack = new cdk.Stack(); @@ -208,19 +208,19 @@ test('encodes booleans', () => { // THEN expect(stack).toHaveResource('Custom::ServiceAction', { - 'Create': { + 'Create': JSON.stringify({ 'service': 'service', 'action': 'action', 'parameters': { - 'trueBoolean': 'TRUE:BOOLEAN', + 'trueBoolean': true, 'trueString': 'true', - 'falseBoolean': 'FALSE:BOOLEAN', + 'falseBoolean': false, 'falseString': 'false', }, 'physicalResourceId': { 'id': 'id', }, - }, + }), }); }); @@ -264,20 +264,20 @@ test('encodes physical resource id reference', () => { // THEN expect(stack).toHaveResource('Custom::ServiceAction', { - 'Create': { + 'Create': JSON.stringify({ 'service': 'service', 'action': 'action', 'parameters': { - 'trueBoolean': 'TRUE:BOOLEAN', + 'trueBoolean': true, 'trueString': 'true', - 'falseBoolean': 'FALSE:BOOLEAN', + 'falseBoolean': false, 'falseString': 'false', 'physicalResourceIdReference': 'PHYSICAL:RESOURCEID:', }, 'physicalResourceId': { 'id': 'id', }, - }, + }), }); }); @@ -526,19 +526,19 @@ test('getDataString', () => { // THEN expect(stack).toHaveResource('Custom::AWS', { Create: { - service: 'service', - action: 'action', - parameters: { - a: { - 'Fn::GetAtt': [ - 'AwsSdk155B91071', - 'Data', - ], - }, - }, - physicalResourceId: { - 'id': 'id', - }, + 'Fn::Join': [ + '', + [ + '{"service":"service","action":"action","parameters":{"a":"', + { + 'Fn::GetAtt': [ + 'AwsSdk155B91071', + 'Data', + ], + }, + '"},"physicalResourceId":{"id":"id"}}', + ], + ], }, }); }); @@ -665,3 +665,50 @@ test('separate policies per custom resource', () => { }, }); }); + +test('tokens can be used as dictionary keys', () => { + // GIVEN + const stack = new cdk.Stack(); + const dummy = new cdk.CfnResource(stack, 'MyResource', { + type: 'AWS::My::Resource', + }); + + // WHEN + new AwsCustomResource(stack, 'Custom1', { + onCreate: { + service: 'service1', + action: 'action1', + physicalResourceId: PhysicalResourceId.of('id1'), + parameters: { + [dummy.ref]: { + Foo: 1234, + Bar: dummy.getAtt('Foorz'), + }, + }, + }, + policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }), + }); + + // THEN + expect(stack).toHaveResource('Custom::AWS', { + Create: { + 'Fn::Join': [ + '', + [ + '{"service":"service1","action":"action1","physicalResourceId":{"id":"id1"},"parameters":{"', + { + 'Ref': 'MyResource', + }, + '":{"Foo":1234,"Bar":"', + { + 'Fn::GetAtt': [ + 'MyResource', + 'Foorz', + ], + }, + '"}}}', + ], + ], + }, + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/integ.aws-custom-resource.expected.json b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/integ.aws-custom-resource.expected.json index 81da053efe11d..2cad60974266d 100644 --- a/packages/@aws-cdk/custom-resources/test/aws-custom-resource/integ.aws-custom-resource.expected.json +++ b/packages/@aws-cdk/custom-resources/test/aws-custom-resource/integ.aws-custom-resource.expected.json @@ -3,6 +3,27 @@ "TopicBFC7AF6E": { "Type": "AWS::SNS::Topic" }, + "PublishCustomResourcePolicyDF696FCA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PublishCustomResourcePolicyDF696FCA", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + } + }, "Publish2E9BDF73": { "Type": "Custom::SNSPublisher", "Properties": { @@ -13,42 +34,44 @@ ] }, "Create": { - "service": "SNS", - "action": "publish", - "parameters": { - "Message": "hello", - "TopicArn": { - "Ref": "TopicBFC7AF6E" - } - }, - "physicalResourceId": { - "id": { - "Ref": "TopicBFC7AF6E" - } - } + "Fn::Join": [ + "", + [ + "{\"service\":\"SNS\",\"action\":\"publish\",\"parameters\":{\"Message\":\"hello\",\"TopicArn\":\"", + { + "Ref": "TopicBFC7AF6E" + }, + "\"},\"physicalResourceId\":{\"id\":\"", + { + "Ref": "TopicBFC7AF6E" + }, + "\"}}" + ] + ] }, "Update": { - "service": "SNS", - "action": "publish", - "parameters": { - "Message": "hello", - "TopicArn": { - "Ref": "TopicBFC7AF6E" - } - }, - "physicalResourceId": { - "id": { - "Ref": "TopicBFC7AF6E" - } - } + "Fn::Join": [ + "", + [ + "{\"service\":\"SNS\",\"action\":\"publish\",\"parameters\":{\"Message\":\"hello\",\"TopicArn\":\"", + { + "Ref": "TopicBFC7AF6E" + }, + "\"},\"physicalResourceId\":{\"id\":\"", + { + "Ref": "TopicBFC7AF6E" + }, + "\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete", "DependsOn": [ "PublishCustomResourcePolicyDF696FCA" - ] + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" }, "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": { "Type": "AWS::IAM::Role", @@ -86,7 +109,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68bS3BucketA4761502" + "Ref": "AssetParametersbd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edbaS3BucketACF45CC2" }, "S3Key": { "Fn::Join": [ @@ -99,7 +122,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68bS3VersionKeyB049B315" + "Ref": "AssetParametersbd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edbaS3VersionKeyBCA0A3F3" } ] } @@ -112,7 +135,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68bS3VersionKeyB049B315" + "Ref": "AssetParametersbd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edbaS3VersionKeyBCA0A3F3" } ] } @@ -122,13 +145,13 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs12.x", "Timeout": 120 }, @@ -136,6 +159,30 @@ "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" ] }, + "ListTopicsCustomResourcePolicy31A8396A": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:ListTopics", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ListTopicsCustomResourcePolicy31A8396A", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + }, + "DependsOn": [ + "TopicBFC7AF6E" + ] + }, "ListTopicsCE1E0341": { "Type": "Custom::AWS", "Properties": { @@ -145,20 +192,8 @@ "Arn" ] }, - "Create": { - "service": "SNS", - "action": "listTopics", - "physicalResourceId": { - "responsePath": "Topics.0.TopicArn" - } - }, - "Update": { - "service": "SNS", - "action": "listTopics", - "physicalResourceId": { - "responsePath": "Topics.0.TopicArn" - } - }, + "Create": "{\"service\":\"SNS\",\"action\":\"listTopics\",\"physicalResourceId\":{\"responsePath\":\"Topics.0.TopicArn\"}}", + "Update": "{\"service\":\"SNS\",\"action\":\"listTopics\",\"physicalResourceId\":{\"responsePath\":\"Topics.0.TopicArn\"}}", "InstallLatestAwsSdk": true }, "DependsOn": [ @@ -175,6 +210,27 @@ "Value": "1337" } }, + "GetParameterCustomResourcePolicyD8E5D455": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ssm:GetParameter", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "GetParameterCustomResourcePolicyD8E5D455", + "Roles": [ + { + "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2" + } + ] + } + }, "GetParameter42B0A00E": { "Type": "Custom::SSMParameter", "Properties": { @@ -185,86 +241,50 @@ ] }, "Create": { - "service": "SSM", - "action": "getParameter", - "parameters": { - "Name": { - "Ref": "DummyParameter53662B67" - }, - "WithDecryption": "TRUE:BOOLEAN" - }, - "physicalResourceId": { - "responsePath": "Parameter.ARN" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"SSM\",\"action\":\"getParameter\",\"parameters\":{\"Name\":\"", + { + "Ref": "DummyParameter53662B67" + }, + "\",\"WithDecryption\":true},\"physicalResourceId\":{\"responsePath\":\"Parameter.ARN\"}}" + ] + ] }, "Update": { - "service": "SSM", - "action": "getParameter", - "parameters": { - "Name": { - "Ref": "DummyParameter53662B67" - }, - "WithDecryption": "TRUE:BOOLEAN" - }, - "physicalResourceId": { - "responsePath": "Parameter.ARN" - } + "Fn::Join": [ + "", + [ + "{\"service\":\"SSM\",\"action\":\"getParameter\",\"parameters\":{\"Name\":\"", + { + "Ref": "DummyParameter53662B67" + }, + "\",\"WithDecryption\":true},\"physicalResourceId\":{\"responsePath\":\"Parameter.ARN\"}}" + ] + ] }, "InstallLatestAwsSdk": true }, - "UpdateReplacePolicy": "Delete", - "DeletionPolicy": "Delete", "DependsOn": [ "GetParameterCustomResourcePolicyD8E5D455" - ] - }, - "PublishCustomResourcePolicyDF696FCA": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [{"Action":"sns:Publish","Effect":"Allow","Resource":"*"}], - "Version": "2012-10-17" - }, - "PolicyName": "PublishCustomResourcePolicyDF696FCA", - "Roles": [{"Ref":"AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2"}] - } - }, - "ListTopicsCustomResourcePolicy31A8396A": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [{"Action":"sns:ListTopics","Effect":"Allow","Resource":"*"}], - "Version": "2012-10-17" - }, - "PolicyName": "ListTopicsCustomResourcePolicy31A8396A", - "Roles": [{"Ref":"AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2"}] - }, - "DependsOn": ["TopicBFC7AF6E"] - }, - "GetParameterCustomResourcePolicyD8E5D455": { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [{"Action":"ssm:GetParameter","Effect":"Allow","Resource":"*"}], - "Version": "2012-10-17" - }, - "PolicyName": "GetParameterCustomResourcePolicyD8E5D455", - "Roles": [{"Ref":"AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2"}] - } + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" } }, "Parameters": { - "AssetParameters0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68bS3BucketA4761502": { + "AssetParametersbd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edbaS3BucketACF45CC2": { "Type": "String", - "Description": "S3 bucket for asset \"0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68b\"" + "Description": "S3 bucket for asset \"bd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edba\"" }, - "AssetParameters0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68bS3VersionKeyB049B315": { + "AssetParametersbd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edbaS3VersionKeyBCA0A3F3": { "Type": "String", - "Description": "S3 key for asset version \"0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68b\"" + "Description": "S3 key for asset version \"bd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edba\"" }, - "AssetParameters0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68bArtifactHash840E5880": { + "AssetParametersbd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edbaArtifactHashF3AE56EF": { "Type": "String", - "Description": "Artifact hash for asset \"0fb8d27cb9854a09fcc93f73882cc5df12ac4108ab02581ab449da4df345f68b\"" + "Description": "Artifact hash for asset \"bd060cb930079c194320bc9a045d159066215c3a4858c45bdb12a79ef9a1edba\"" } }, "Outputs": { diff --git a/packages/@aws-cdk/cx-api/lib/features.ts b/packages/@aws-cdk/cx-api/lib/features.ts index 24ba471882644..162ec0de9d7f4 100644 --- a/packages/@aws-cdk/cx-api/lib/features.ts +++ b/packages/@aws-cdk/cx-api/lib/features.ts @@ -89,6 +89,21 @@ export const KMS_DEFAULT_KEY_POLICIES = '@aws-cdk/aws-kms:defaultKeyPolicies'; */ export const S3_GRANT_WRITE_WITHOUT_ACL = '@aws-cdk/aws-s3:grantWriteWithoutAcl'; +/** + * ApplicationLoadBalancedServiceBase, ApplicationMultipleTargetGroupServiceBase, + * NetworkLoadBalancedServiceBase, NetworkMultipleTargetGroupServiceBase, and + * QueueProcessingServiceBase currently determine a default value for the desired count of + * a CfnService if a desiredCount is not provided. + * + * If this flag is not set, the default behaviour for CfnService.desiredCount is to set a + * desiredCount of 1, if one is not provided. If true, a default will not be defined for + * CfnService.desiredCount and as such desiredCount will be undefined, if one is not provided. + * + * This is a feature flag as the old behavior was technically incorrect, but + * users may have come to depend on it. + */ +export const ECS_REMOVE_DEFAULT_DESIRED_COUNT = '@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount'; + /** * This map includes context keys and values for feature flags that enable * capabilities "from the future", which we could not introduce as the default @@ -110,6 +125,7 @@ export const FUTURE_FLAGS: { [key: string]: any } = { [SECRETS_MANAGER_PARSE_OWNED_SECRET_NAME]: true, [KMS_DEFAULT_KEY_POLICIES]: true, [S3_GRANT_WRITE_WITHOUT_ACL]: true, + [ECS_REMOVE_DEFAULT_DESIRED_COUNT]: true, // We will advertise this flag when the feature is complete // [NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: 'true', @@ -135,6 +151,7 @@ const FUTURE_FLAGS_DEFAULTS: { [key: string]: boolean } = { [SECRETS_MANAGER_PARSE_OWNED_SECRET_NAME]: false, [KMS_DEFAULT_KEY_POLICIES]: false, [S3_GRANT_WRITE_WITHOUT_ACL]: false, + [ECS_REMOVE_DEFAULT_DESIRED_COUNT]: false, }; export function futureFlagDefault(flag: string): boolean { diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 718ba9b503690..512ee5e97c92b 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -655,6 +655,12 @@ These command lines explained: > Make sure you trust all the code and dependencies that make up your CDK app. > Check with the appropriate department within your organization to decide on the > proper policy to use. +> +> If your policy includes permissions to create on attach permission to a role, +> developers can escalate their privilege with more permissive permission. +> Thus, we recommend implementing [permissions boundary](https://aws.amazon.com/premiumsupport/knowledge-center/iam-permission-boundaries/) +> in the CDK Execution role. To do this, you can bootstrap with the `--template` option with +> [a customized template](https://github.com/aws-samples/aws-bootstrap-kit-examples/blob/ba28a97d289128281bc9483bcba12c1793f2c27a/source/1-SDLC-organization/lib/cdk-bootstrap-template.yml#L395) that contains a permission boundary. ### Migrating from old bootstrap stack diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index 86806f041880f..f70aec783eff1 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -41,13 +41,13 @@ "jest": "^26.6.3", "monocdk": "0.0.0", "pkglint": "0.0.0", - "ts-jest": "^26.5.1" + "ts-jest": "^26.5.3" }, "dependencies": { "@aws-cdk/cloudformation-diff": "0.0.0" }, "peerDependencies": { - "constructs": "^3.0.4", + "constructs": "^3.2.0", "jest": "^26.6.3", "monocdk": "^0.0.0" }, diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index 6eadd958185df..89dbc64979387 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -293,7 +293,7 @@ "ubergen": "0.0.0" }, "peerDependencies": { - "constructs": "^3.0.4" + "constructs": "^3.2.0" }, "homepage": "https://github.com/aws/aws-cdk", "engines": { diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 78e2177b186cd..41dd4c75fbe2a 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -207,7 +207,7 @@ Usage of output in a CDK stack const fn = new lambda.Function(this, "fn", { handler: "index.handler", code: lambda.Code.fromInline(`exports.handler = \${handler.toString()}`), - runtime: lambda.Runtime.NODEJS_10_X + runtime: lambda.Runtime.NODEJS_12_X }); new cdk.CfnOutput(this, 'FunctionArn', { @@ -288,13 +288,14 @@ The `progress` key can also be specified as a user setting (`~/.cdk.json`) #### Externally Executable CloudFormation Change Sets For more control over when stack changes are deployed, the CDK can generate a -CloudFormation change set but not execute it. The name of the generated +CloudFormation change set but not execute it. The default name of the generated change set is *cdk-deploy-change-set*, and a previous change set with that name will be overwritten. The change set will always be created, even if it -is empty. +is empty. A name can also be given to the change set to make it easier to later +execute. ```console -$ cdk deploy --no-execute +$ cdk deploy --no-execute --change-set-name MyChangeSetName ``` ### `cdk destroy` diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 707c4ab770758..9dc3a71799457 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -94,6 +94,7 @@ async function parseCommandLineArguments() { // @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment .option('tags', { type: 'array', alias: 't', desc: 'Tags to add to the stack (KEY=VALUE), overrides tags from Cloud Assembly (deprecated)', nargs: 1, requiresArg: true }) .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }) + .option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create' }) .option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false }) .option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} }) .option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true }) @@ -316,6 +317,7 @@ async function initCommandLine() { reuseAssets: args['build-exclude'], tags: configuration.settings.get(['tags']), execute: args.execute, + changeSetName: args.changeSetName, force: args.force, parameters: parameterMap, usePreviousParameters: args['previous-parameters'], diff --git a/packages/aws-cdk/lib/api/cloudformation-deployments.ts b/packages/aws-cdk/lib/api/cloudformation-deployments.ts index 3fe5fed118a76..3dde581f0b514 100644 --- a/packages/aws-cdk/lib/api/cloudformation-deployments.ts +++ b/packages/aws-cdk/lib/api/cloudformation-deployments.ts @@ -69,6 +69,12 @@ export interface DeployStackOptions { */ execute?: boolean; + /** + * Optional name to use for the CloudFormation change set. + * If not provided, a name will be generated automatically. + */ + changeSetName?: string; + /** * Force deployment, even if the deployed template is identical to the one we are about to deploy. * @default false deployment will be skipped if the template is identical @@ -173,6 +179,7 @@ export class CloudFormationDeployments { toolkitInfo, tags: options.tags, execute: options.execute, + changeSetName: options.changeSetName, force: options.force, parameters: options.parameters, usePreviousParameters: options.usePreviousParameters, diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 2dfb2f5119c71..177df82d4fcbf 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -131,6 +131,12 @@ export interface DeployStackOptions { */ execute?: boolean; + /** + * Optional name to use for the CloudFormation change set. + * If not provided, a name will be generated automatically. + */ + changeSetName?: string; + /** * The collection of extra parameters * (in addition to those used for assets) @@ -174,7 +180,6 @@ export interface DeployStackOptions { } const LARGE_TEMPLATE_SIZE_KB = 50; -const CDK_CHANGE_SET_NAME = 'cdk-deploy-change-set'; /** @experimental */ export async function deployStack(options: DeployStackOptions): Promise { @@ -229,20 +234,21 @@ export async function deployStack(options: DeployStackOptions): Promise UTF-8 %cdk-version% - 5.7.0 + 5.7.1 diff --git a/packages/aws-cdk/lib/init-templates/v1/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py b/packages/aws-cdk/lib/init-templates/v1/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py index 9d86ad16906e6..6b4ed6e8ea6ed 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py +++ b/packages/aws-cdk/lib/init-templates/v1/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py @@ -1,9 +1,15 @@ +from aws_cdk import core as cdk + +# For consistency with other languages, `cdk` is the preferred import name for +# the CDK's core module. The following line also imports it as `core` for use +# with examples from the CDK Developer's Guide, which are in the process of +# being updated to use `cdk`. You may delete this import if you don't need it. from aws_cdk import core -class %name.PascalCased%Stack(core.Stack): +class %name.PascalCased%Stack(cdk.Stack): - def __init__(self, scope: core.Construct, construct_id: str, **kwargs) -> None: + def __init__(self, scope: cdk.Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # The code that defines your stack goes here diff --git a/packages/aws-cdk/lib/init-templates/v1/app/python/app.template.py b/packages/aws-cdk/lib/init-templates/v1/app/python/app.template.py index 808bc22af32e4..bc43099fd2026 100644 --- a/packages/aws-cdk/lib/init-templates/v1/app/python/app.template.py +++ b/packages/aws-cdk/lib/init-templates/v1/app/python/app.template.py @@ -1,11 +1,17 @@ #!/usr/bin/env python3 +from aws_cdk import core as cdk + +# For consistency with TypeScript code, `cdk` is the preferred import name for +# the CDK's core module. The following line also imports it as `core` for use +# with examples from the CDK Developer's Guide, which are in the process of +# being updated to use `cdk`. You may delete this import if you don't need it. from aws_cdk import core from %name.PythonModule%.%name.PythonModule%_stack import %name.PascalCased%Stack -app = core.App() +app = cdk.App() %name.PascalCased%Stack(app, "%name.StackName%") app.synth() diff --git a/packages/aws-cdk/lib/init-templates/v1/sample-app/java/pom.template.xml b/packages/aws-cdk/lib/init-templates/v1/sample-app/java/pom.template.xml index 0355825bf0a7a..5d679d2570040 100644 --- a/packages/aws-cdk/lib/init-templates/v1/sample-app/java/pom.template.xml +++ b/packages/aws-cdk/lib/init-templates/v1/sample-app/java/pom.template.xml @@ -8,7 +8,7 @@ UTF-8 %cdk-version% - 5.7.0 + 5.7.1 diff --git a/packages/aws-cdk/lib/init-templates/v2/app/java/pom.template.xml b/packages/aws-cdk/lib/init-templates/v2/app/java/pom.template.xml index f49fe7b60e963..5defab0b3a0b6 100644 --- a/packages/aws-cdk/lib/init-templates/v2/app/java/pom.template.xml +++ b/packages/aws-cdk/lib/init-templates/v2/app/java/pom.template.xml @@ -10,7 +10,7 @@ UTF-8 %cdk-version% - 5.7.0 + 5.7.1 diff --git a/packages/aws-cdk/lib/init-templates/v2/app/python/.template.gitignore b/packages/aws-cdk/lib/init-templates/v2/app/python/.template.gitignore index 383cdd5040f7e..37833f8beb2a3 100644 --- a/packages/aws-cdk/lib/init-templates/v2/app/python/.template.gitignore +++ b/packages/aws-cdk/lib/init-templates/v2/app/python/.template.gitignore @@ -2,7 +2,7 @@ package-lock.json __pycache__ .pytest_cache -.env +.venv *.egg-info # CDK asset staging directory diff --git a/packages/aws-cdk/lib/init-templates/v2/sample-app/java/pom.template.xml b/packages/aws-cdk/lib/init-templates/v2/sample-app/java/pom.template.xml index 1236d332e9bab..7f10c89a54dd4 100644 --- a/packages/aws-cdk/lib/init-templates/v2/sample-app/java/pom.template.xml +++ b/packages/aws-cdk/lib/init-templates/v2/sample-app/java/pom.template.xml @@ -8,7 +8,7 @@ UTF-8 %cdk-version% - 5.7.0 + 5.7.1 diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index a3f34cdacf9d4..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.2.0", + "@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.7", + "nock": "^13.0.10", "pkglint": "0.0.0", "sinon": "^9.2.4", - "ts-jest": "^26.5.1", + "ts-jest": "^26.5.3", "ts-mock-imports": "^1.3.3", "xml-js": "^1.6.11" }, diff --git a/packages/aws-cdk/test/integ/cli/cli.integtest.ts b/packages/aws-cdk/test/integ/cli/cli.integtest.ts index 087efd00eb524..eddce24e7a778 100644 --- a/packages/aws-cdk/test/integ/cli/cli.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/cli.integtest.ts @@ -139,9 +139,10 @@ integTest('nested stack with parameters', withDefaultFixture(async (fixture) => expect(response.StackResources?.length).toEqual(1); })); -integTest('deploy without execute', withDefaultFixture(async (fixture) => { +integTest('deploy without execute a named change set', withDefaultFixture(async (fixture) => { + const changeSetName = 'custom-change-set-name'; const stackArn = await fixture.cdkDeploy('test-2', { - options: ['--no-execute'], + options: ['--no-execute', '--change-set-name', changeSetName], captureStderr: false, }); // verify that we only deployed a single stack (there's a single ARN in the output) @@ -150,8 +151,16 @@ integTest('deploy without execute', withDefaultFixture(async (fixture) => { const response = await fixture.aws.cloudFormation('describeStacks', { StackName: stackArn, }); - expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); + + //verify a change set was created with the provided name + const changeSetResponse = await fixture.aws.cloudFormation('listChangeSets', { + StackName: stackArn, + }); + const changeSets = changeSetResponse.Summaries || []; + expect(changeSets.length).toEqual(1); + expect(changeSets[0].ChangeSetName).toEqual(changeSetName); + expect(changeSets[0].Status).toEqual('CREATE_COMPLETE'); })); integTest('security related changes without a CLI are expected to fail', withDefaultFixture(async (fixture) => { diff --git a/packages/awslint/package.json b/packages/awslint/package.json index f0a74898f0148..3be540173745f 100644 --- a/packages/awslint/package.json +++ b/packages/awslint/package.json @@ -16,11 +16,11 @@ "awslint": "bin/awslint" }, "dependencies": { - "@jsii/spec": "^1.21.0", + "@jsii/spec": "^1.24.0", "camelcase": "^6.2.0", "colors": "^1.4.0", "fs-extra": "^9.1.0", - "jsii-reflect": "^1.21.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 917a0b25715aa..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.21.0", + "codemaker": "^1.24.0", "yaml": "1.10.0" }, "devDependencies": { diff --git a/packages/decdk/package.json b/packages/decdk/package.json index ec918d8d77feb..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.21.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.21.0" + "jsii": "^1.24.0" }, "keywords": [ "aws", diff --git a/scripts/check-pack-prerequisites.sh b/scripts/check-pack-prerequisites.sh index 8dca0902114e5..6b648054fe253 100755 --- a/scripts/check-pack-prerequisites.sh +++ b/scripts/check-pack-prerequisites.sh @@ -54,12 +54,12 @@ app_v=$(${app} -version 2>&1) echo -e "Checking javac version... \c" # 1.8 if [ $(echo $app_v | grep -c -E "1\.8\.[0-9].*") -eq 1 ] -then +then echo "Ok" else # 11 or 14 or 15 if [ $(echo $app_v | grep -c -E "1[145]\.[0-9]\.[0-9].*") -eq 1 ] - then + then echo "Ok" else wrong_version @@ -73,7 +73,7 @@ check_which $app $app_min app_v=$(${app} --version) echo -e "Checking mvn version... \c" if [ $(echo $app_v | grep -c -E "3\.[6789]\.[0-9].*") -eq 1 ] -then +then echo "Ok" else wrong_version @@ -85,8 +85,8 @@ app_min="3.1.0" check_which $app $app_min app_v=$(${app} --version) echo -e "Checking $app version... \c" -if [ $(echo $app_v | grep -c -E "3\.1\.[0-9].*") -eq 1 ] -then +if [ $(echo $app_v | grep -c -E "3\.1\.[0-9].*|[4-9]\..*") -eq 1 ] +then echo "Ok" else wrong_version @@ -99,7 +99,7 @@ check_which $app $app_min app_v=$(${app} --version) echo -e "Checking $app version... \c" if [ $(echo $app_v | grep -c -E "3\.[6789]\.[0-9].*") -eq 1 ] -then +then echo "Ok" else wrong_version diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 8b2a6236620e9..645d6eb54b454 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -39,11 +39,11 @@ "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^4.15.1", - "@typescript-eslint/parser": "^4.15.1", + "@typescript-eslint/eslint-plugin": "^4.16.1", + "@typescript-eslint/parser": "^4.16.1", "awslint": "0.0.0", "colors": "^1.4.0", - "eslint": "^7.20.0", + "eslint": "^7.21.0", "eslint-import-resolver-node": "^0.3.4", "eslint-import-resolver-typescript": "^2.4.0", "eslint-plugin-cdk": "0.0.0", @@ -51,13 +51,13 @@ "eslint-plugin-jest": "^24.1.5", "fs-extra": "^9.1.0", "jest": "^26.6.3", - "jsii": "^1.21.0", - "jsii-pacmak": "^1.21.0", - "markdownlint-cli": "^0.26.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.1", + "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 7a62104df3ea9..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.21.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/eslint-plugin-cdk/package.json b/tools/eslint-plugin-cdk/package.json index 2c412f30e739e..f21cb470ca5f5 100644 --- a/tools/eslint-plugin-cdk/package.json +++ b/tools/eslint-plugin-cdk/package.json @@ -21,8 +21,8 @@ "typescript": "~3.9.9" }, "dependencies": { - "@typescript-eslint/parser": "^4.15.1", - "eslint": "^7.20.0", + "@typescript-eslint/parser": "^4.16.1", + "eslint": "^7.21.0", "fs-extra": "^9.1.0" }, "jest": { diff --git a/tools/pkglint/lib/rules.ts b/tools/pkglint/lib/rules.ts index 67e26c9634712..1dd32c1c96392 100644 --- a/tools/pkglint/lib/rules.ts +++ b/tools/pkglint/lib/rules.ts @@ -617,6 +617,38 @@ export class NoPeerDependenciesMonocdk extends ValidationRule { } } +/** + * Validates that the same version of `constructs` is used wherever a dependency + * is specified, so that they must all be udpated at the same time (through an + * update to this rule). + * + * Note: v1 and v2 use different versions respectively. + */ +export class ConstructsVersion extends ValidationRule { + public readonly name = 'deps/constructs'; + private readonly expectedRange = cdkMajorVersion() === 2 + ? '10.0.0-pre.5' + : '^3.2.0'; + + public validate(pkg: PackageJson) { + const toCheck = new Array(); + + if ('constructs' in pkg.dependencies) { + toCheck.push('dependencies'); + } + if ('constructs' in pkg.devDependencies) { + toCheck.push('devDependencies'); + } + if ('constructs' in pkg.peerDependencies) { + toCheck.push('peerDependencies'); + } + + for (const cfg of toCheck) { + expectJSON(this.name, pkg, `${cfg}.constructs`, this.expectedRange); + } + } +} + /** * JSII Java package is required and must look sane */ 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 0044085ed9ce5..9e33a1efe99be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,36 +16,52 @@ dependencies: "@babel/highlight" "^7.12.13" +"@babel/compat-data@^7.13.8": + version "7.13.8" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.8.tgz#5b783b9808f15cef71547f1b691f34f8ff6003a6" + integrity sha512-EaI33z19T4qN3xLXsGf48M2cDqa6ei9tPZlfLdb2HC+e/cFtREiRd8hdSqDbwdLB0/+gLwqJmCYASH0z2bUdog== + "@babel/core@^7.1.0", "@babel/core@^7.7.5": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.17.tgz#993c5e893333107a2815d8e0d73a2c3755e280b2" - integrity sha512-V3CuX1aBywbJvV2yzJScRxeiiw0v2KZZYYE3giywxzFJL13RiyPjaaDwhDnxmgFTTS7FgvM2ijr4QmKNIu0AtQ== + version "7.13.8" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.8.tgz#c191d9c5871788a591d69ea1dc03e5843a3680fb" + integrity sha512-oYapIySGw1zGhEFRd6lzWNLWFX2s5dA/jm+Pw/+59ZdXtjyIuwlXbrId22Md0rgZVop+aVoqow2riXhBLNyuQg== dependencies: "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.12.17" - "@babel/helper-module-transforms" "^7.12.17" - "@babel/helpers" "^7.12.17" - "@babel/parser" "^7.12.17" + "@babel/generator" "^7.13.0" + "@babel/helper-compilation-targets" "^7.13.8" + "@babel/helper-module-transforms" "^7.13.0" + "@babel/helpers" "^7.13.0" + "@babel/parser" "^7.13.4" "@babel/template" "^7.12.13" - "@babel/traverse" "^7.12.17" - "@babel/types" "^7.12.17" + "@babel/traverse" "^7.13.0" + "@babel/types" "^7.13.0" convert-source-map "^1.7.0" debug "^4.1.0" - gensync "^1.0.0-beta.1" + gensync "^1.0.0-beta.2" json5 "^2.1.2" lodash "^4.17.19" - semver "^5.4.1" + semver "^6.3.0" source-map "^0.5.0" -"@babel/generator@^7.12.17", "@babel/generator@^7.4.0": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.17.tgz#9ef1dd792d778b32284411df63f4f668a9957287" - integrity sha512-DSA7ruZrY4WI8VxuS1jWSRezFnghEoYEFrZcw9BizQRmOZiUsiHl59+qEARGPqPikwA/GPTyRCi7isuCK/oyqg== +"@babel/generator@^7.13.0", "@babel/generator@^7.4.0": + version "7.13.9" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39" + integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw== dependencies: - "@babel/types" "^7.12.17" + "@babel/types" "^7.13.0" jsesc "^2.5.1" source-map "^0.5.0" +"@babel/helper-compilation-targets@^7.13.8": + version "7.13.8" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.8.tgz#02bdb22783439afb11b2f009814bdd88384bd468" + integrity sha512-pBljUGC1y3xKLn1nrx2eAhurLMA8OqBtBP/JwG4U8skN7kf8/aqwwxpV1N6T0e7r6+7uNitIa/fUxPFagSXp3A== + dependencies: + "@babel/compat-data" "^7.13.8" + "@babel/helper-validator-option" "^7.12.17" + browserslist "^4.14.5" + semver "^6.3.0" + "@babel/helper-function-name@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a" @@ -62,12 +78,12 @@ dependencies: "@babel/types" "^7.12.13" -"@babel/helper-member-expression-to-functions@^7.12.13": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.17.tgz#f82838eb06e1235307b6d71457b6670ff71ee5ac" - integrity sha512-Bzv4p3ODgS/qpBE0DiJ9qf5WxSmrQ8gVTe8ClMfwwsY2x/rhykxxy3bXzG7AGTnPB2ij37zGJ/Q/6FruxHxsxg== +"@babel/helper-member-expression-to-functions@^7.13.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.0.tgz#6aa4bb678e0f8c22f58cdb79451d30494461b091" + integrity sha512-yvRf8Ivk62JwisqV1rFRMxiSMDGnN6KH1/mDMmIrij4jztpQNRoHqqMG3U6apYbGRPJpgPalhva9Yd06HlUxJQ== dependencies: - "@babel/types" "^7.12.17" + "@babel/types" "^7.13.0" "@babel/helper-module-imports@^7.12.13": version "7.12.13" @@ -76,19 +92,19 @@ dependencies: "@babel/types" "^7.12.13" -"@babel/helper-module-transforms@^7.12.17": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.12.17.tgz#7c75b987d6dfd5b48e575648f81eaac891539509" - integrity sha512-sFL+p6zOCQMm9vilo06M4VHuTxUAwa6IxgL56Tq1DVtA0ziAGTH1ThmJq7xwPqdQlgAbKX3fb0oZNbtRIyA5KQ== +"@babel/helper-module-transforms@^7.13.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.0.tgz#42eb4bd8eea68bab46751212c357bfed8b40f6f1" + integrity sha512-Ls8/VBwH577+pw7Ku1QkUWIyRRNHpYlts7+qSqBBFCW3I8QteB9DxfcZ5YJpOwH6Ihe/wn8ch7fMGOP1OhEIvw== dependencies: "@babel/helper-module-imports" "^7.12.13" - "@babel/helper-replace-supers" "^7.12.13" + "@babel/helper-replace-supers" "^7.13.0" "@babel/helper-simple-access" "^7.12.13" "@babel/helper-split-export-declaration" "^7.12.13" "@babel/helper-validator-identifier" "^7.12.11" "@babel/template" "^7.12.13" - "@babel/traverse" "^7.12.17" - "@babel/types" "^7.12.17" + "@babel/traverse" "^7.13.0" + "@babel/types" "^7.13.0" lodash "^4.17.19" "@babel/helper-optimise-call-expression@^7.12.13": @@ -99,19 +115,19 @@ "@babel/types" "^7.12.13" "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.8.0": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.12.13.tgz#174254d0f2424d8aefb4dd48057511247b0a9eeb" - integrity sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA== + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz#806526ce125aed03373bc416a828321e3a6a33af" + integrity sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ== -"@babel/helper-replace-supers@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz#00ec4fb6862546bd3d0aff9aac56074277173121" - integrity sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg== +"@babel/helper-replace-supers@^7.13.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.13.0.tgz#6034b7b51943094cb41627848cb219cb02be1d24" + integrity sha512-Segd5me1+Pz+rmN/NFBOplMbZG3SqRJOBlY+mA0SxAv6rjj7zJqr1AVr3SfzUVTLCv7ZLU5FycOM/SBGuLPbZw== dependencies: - "@babel/helper-member-expression-to-functions" "^7.12.13" + "@babel/helper-member-expression-to-functions" "^7.13.0" "@babel/helper-optimise-call-expression" "^7.12.13" - "@babel/traverse" "^7.12.13" - "@babel/types" "^7.12.13" + "@babel/traverse" "^7.13.0" + "@babel/types" "^7.13.0" "@babel/helper-simple-access@^7.12.13": version "7.12.13" @@ -132,28 +148,33 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== -"@babel/helpers@^7.12.17": +"@babel/helper-validator-option@^7.12.17": version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.12.17.tgz#71e03d2981a6b5ee16899964f4101dc8471d60bc" - integrity sha512-tEpjqSBGt/SFEsFikKds1sLNChKKGGR17flIgQKXH4fG6m9gTgl3gnOC1giHNyaBCSKuTfxaSzHi7UnvqiVKxg== + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831" + integrity sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw== + +"@babel/helpers@^7.13.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.0.tgz#7647ae57377b4f0408bf4f8a7af01c42e41badc0" + integrity sha512-aan1MeFPxFacZeSz6Ld7YZo5aPuqnKlD7+HZY75xQsueczFccP9A7V05+oe0XpLwHK3oLorPe9eaAUljL7WEaQ== dependencies: "@babel/template" "^7.12.13" - "@babel/traverse" "^7.12.17" - "@babel/types" "^7.12.17" + "@babel/traverse" "^7.13.0" + "@babel/types" "^7.13.0" "@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c" - integrity sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww== + version "7.13.8" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.8.tgz#10b2dac78526424dfc1f47650d0e415dfd9dc481" + integrity sha512-4vrIhfJyfNf+lCtXC2ck1rKSzDwciqF7IWFhXXrSOUC2O5DrVp+w4c6ed4AllTxhTkUP5x2tYj41VaxdVMMRDw== dependencies: "@babel/helper-validator-identifier" "^7.12.11" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.12.17", "@babel/parser@^7.4.3": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.17.tgz#bc85d2d47db38094e5bb268fc761716e7d693848" - integrity sha512-r1yKkiUTYMQ8LiEI0UcQx5ETw5dpTLn9wijn9hk6KkTtOK95FndDN10M+8/s6k/Ymlbivw0Av9q4SlgF80PtHg== +"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.0", "@babel/parser@^7.13.4", "@babel/parser@^7.4.3": + version "7.13.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.9.tgz#ca34cb95e1c2dd126863a84465ae8ef66114be99" + integrity sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -248,25 +269,25 @@ "@babel/parser" "^7.12.13" "@babel/types" "^7.12.13" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.12.13", "@babel/traverse@^7.12.17", "@babel/traverse@^7.4.3": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.17.tgz#40ec8c7ffb502c4e54c7f95492dc11b88d718619" - integrity sha512-LGkTqDqdiwC6Q7fWSwQoas/oyiEYw6Hqjve5KOSykXkmFJFqzvGMb9niaUEag3Rlve492Mkye3gLw9FTv94fdQ== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.4.3": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.0.tgz#6d95752475f86ee7ded06536de309a65fc8966cc" + integrity sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ== dependencies: "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.12.17" + "@babel/generator" "^7.13.0" "@babel/helper-function-name" "^7.12.13" "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/parser" "^7.12.17" - "@babel/types" "^7.12.17" + "@babel/parser" "^7.13.0" + "@babel/types" "^7.13.0" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.19" -"@babel/types@^7.0.0", "@babel/types@^7.12.13", "@babel/types@^7.12.17", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.0": - version "7.12.17" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.17.tgz#9d711eb807e0934c90b8b1ca0eb1f7230d150963" - integrity sha512-tNMDjcv/4DIcHxErTgwB9q2ZcYyN0sUfgGKUK/mm1FJK7Wz+KstoEekxrl/tBiNDgLK1HGi+sppj1An/1DR4fQ== +"@babel/types@^7.0.0", "@babel/types@^7.12.13", "@babel/types@^7.13.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.0": + version "7.13.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80" + integrity sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA== dependencies: "@babel/helper-validator-identifier" "^7.12.11" lodash "^4.17.19" @@ -290,10 +311,10 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@eslint/eslintrc@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.3.0.tgz#d736d6963d7003b6514e6324bec9c602ac340318" - integrity sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg== +"@eslint/eslintrc@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.0.tgz#99cc0a0584d72f1df38b900fb062ba995f395547" + integrity sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog== dependencies: ajv "^6.12.4" debug "^4.1.1" @@ -302,7 +323,6 @@ ignore "^4.0.6" import-fresh "^3.2.1" js-yaml "^3.13.1" - lodash "^4.17.20" minimatch "^3.0.4" strip-json-comments "^3.1.1" @@ -567,10 +587,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@jsii/spec@^1.21.0": - version "1.21.0" - resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.21.0.tgz#363c740567747aa03083d2f7bf9ef14e55d8ae9d" - integrity sha512-MWQpJKciYytEmYzuwsT+4UM1JPiQyCAqr3PfkZxuosoPUaF7vBrWSs2+TXDb5dcCwpSnSim9iKZrM/Uc2ppUzA== +"@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" @@ -1330,10 +1350,10 @@ "@octokit/types" "^6.0.3" universal-user-agent "^6.0.0" -"@octokit/openapi-types@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-5.1.0.tgz#661fd03c7d55fbcb0a0937d3353d87dea012f52c" - integrity sha512-bodZvSYgycbUuuKrC/anCBUExvaSSWzMMFz0xl7pcJujxnmGxvqvcFHktjx1ZOSyeNKLfYF0QCgibaHUGsZTng== +"@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" @@ -1348,11 +1368,11 @@ "@octokit/types" "^2.0.1" "@octokit/plugin-paginate-rest@^2.6.2": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.10.0.tgz#5925156d809c94b7bfc47b28e17488415548fa67" - integrity sha512-71OsKBSMcQEu/6lfVbhv5C5ikU1rn10rKot/WiV7do7fyfElQ2eCUQFogHPbj0ci5lnKAjvahOiMAr6lcvL8Qw== + version "2.11.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.11.0.tgz#3568c43896a3355f4a0bbb3a64f443b2abdc760d" + integrity sha512-7L9xQank2G3r1dGqrVPo1z62V5utbykOUzlmNHPz87Pww/JpZQ9KyG5CHtUzgmB4n5iDRKYNK/86A8D98HP0yA== dependencies: - "@octokit/types" "^6.10.0" + "@octokit/types" "^6.11.0" "@octokit/plugin-request-log@^1.0.0", "@octokit/plugin-request-log@^1.0.2": version "1.0.3" @@ -1367,12 +1387,12 @@ "@octokit/types" "^2.0.1" deprecation "^2.3.1" -"@octokit/plugin-rest-endpoint-methods@4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.12.0.tgz#1cec405cd4eaf0bdb58cb7d2a9b3d8473b3a70e8" - integrity sha512-RgnQ1aoetdOJjZYC37LV5FNlL7GY/v1CdC5dur1Zp/UiADJlbRFbAz/xLx26ovXw67dK7EUtwCghS+6QyiI9RA== +"@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.10.0" + "@octokit/types" "^6.11.2" deprecation "^2.3.1" "@octokit/request-error@^1.0.2": @@ -1429,15 +1449,15 @@ once "^1.4.0" universal-user-agent "^4.0.0" -"@octokit/rest@^18.2.0": - version "18.2.0" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.2.0.tgz#b75c87870bb1f7bc9f37ae0e9acb3a411a34a25f" - integrity sha512-xsp6bIqL2sb/NmgLXTxw96caegobRw+YHnzdIi70ruquHtPPDW2cBAONhDYMUuAOeXx0JH2auOeplpk4SQJy1w== +"@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.12.0" + "@octokit/plugin-rest-endpoint-methods" "4.13.2" "@octokit/types@^2.0.0", "@octokit/types@^2.0.1": version "2.16.2" @@ -1446,12 +1466,12 @@ dependencies: "@types/node" ">= 8" -"@octokit/types@^6.0.3", "@octokit/types@^6.10.0", "@octokit/types@^6.7.1": - version "6.10.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.10.0.tgz#243faa864b0955f574012d52e179de38ac9ebafe" - integrity sha512-aMDo10kglofejJ96edCBIgQLVuzMDyjxmhdgEcoUUD64PlHYSrNsAGqN0wZtoiX4/PCQ3JLA50IpkP1bcKD/cA== +"@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.1.0" + "@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" @@ -1585,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== @@ -1665,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" @@ -1761,13 +1781,13 @@ resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.4.tgz#445251eb00bd9c1e751f82c7c6bf4f714edfd464" integrity sha512-/emrKCfQMQmFCqRqqBJ0JueHBT06jBRM3e8OgnvDUcvuExONujIk2hFA5dNsN9Nt41ljGVDdChvCydATZ+KOZw== -"@typescript-eslint/eslint-plugin@^4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.15.1.tgz#835f64aa0a403e5e9e64c10ceaf8d05c3f015180" - integrity sha512-yW2epMYZSpNJXZy22Biu+fLdTG8Mn6b22kR3TqblVk50HGNV8Zya15WAXuQCr8tKw4Qf1BL4QtI6kv6PCkLoJw== +"@typescript-eslint/eslint-plugin@^4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz#2caf6a79dd19c3853b8d39769a27fccb24e4e651" + integrity sha512-SK777klBdlkUZpZLC1mPvyOWk9yAFCWmug13eAjVQ4/Q1LATE/NbcQL1xDHkptQkZOLnPmLUA1Y54m8dqYwnoQ== dependencies: - "@typescript-eslint/experimental-utils" "4.15.1" - "@typescript-eslint/scope-manager" "4.15.1" + "@typescript-eslint/experimental-utils" "4.16.1" + "@typescript-eslint/scope-manager" "4.16.1" debug "^4.1.1" functional-red-black-tree "^1.0.1" lodash "^4.17.15" @@ -1775,60 +1795,60 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@4.15.1", "@typescript-eslint/experimental-utils@^4.0.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.15.1.tgz#d744d1ac40570a84b447f7aa1b526368afd17eec" - integrity sha512-9LQRmOzBRI1iOdJorr4jEnQhadxK4c9R2aEAsm7WE/7dq8wkKD1suaV0S/JucTL8QlYUPU1y2yjqg+aGC0IQBQ== +"@typescript-eslint/experimental-utils@4.16.1", "@typescript-eslint/experimental-utils@^4.0.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.16.1.tgz#da7a396dc7d0e01922acf102b76efff17320b328" + integrity sha512-0Hm3LSlMYFK17jO4iY3un1Ve9x1zLNn4EM50Lia+0EV99NdbK+cn0er7HC7IvBA23mBg3P+8dUkMXy4leL33UQ== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.15.1" - "@typescript-eslint/types" "4.15.1" - "@typescript-eslint/typescript-estree" "4.15.1" + "@typescript-eslint/scope-manager" "4.16.1" + "@typescript-eslint/types" "4.16.1" + "@typescript-eslint/typescript-estree" "4.16.1" eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.15.1.tgz#4c91a0602733db63507e1dbf13187d6c71a153c4" - integrity sha512-V8eXYxNJ9QmXi5ETDguB7O9diAXlIyS+e3xzLoP/oVE4WCAjssxLIa0mqCLsCGXulYJUfT+GV70Jv1vHsdKwtA== +"@typescript-eslint/parser@^4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.16.1.tgz#3bbd3234dd3c5b882b2bcd9899bc30e1e1586d2a" + integrity sha512-/c0LEZcDL5y8RyI1zLcmZMvJrsR6SM1uetskFkoh3dvqDKVXPsXI+wFB/CbVw7WkEyyTKobC1mUNp/5y6gRvXg== dependencies: - "@typescript-eslint/scope-manager" "4.15.1" - "@typescript-eslint/types" "4.15.1" - "@typescript-eslint/typescript-estree" "4.15.1" + "@typescript-eslint/scope-manager" "4.16.1" + "@typescript-eslint/types" "4.16.1" + "@typescript-eslint/typescript-estree" "4.16.1" debug "^4.1.1" -"@typescript-eslint/scope-manager@4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.15.1.tgz#f6511eb38def2a8a6be600c530c243bbb56ac135" - integrity sha512-ibQrTFcAm7yG4C1iwpIYK7vDnFg+fKaZVfvyOm3sNsGAerKfwPVFtYft5EbjzByDJ4dj1WD8/34REJfw/9wdVA== +"@typescript-eslint/scope-manager@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.16.1.tgz#244e2006bc60cfe46987e9987f4ff49c9e3f00d5" + integrity sha512-6IlZv9JaurqV0jkEg923cV49aAn8V6+1H1DRfhRcvZUrptQ+UtSKHb5kwTayzOYTJJ/RsYZdcvhOEKiBLyc0Cw== dependencies: - "@typescript-eslint/types" "4.15.1" - "@typescript-eslint/visitor-keys" "4.15.1" + "@typescript-eslint/types" "4.16.1" + "@typescript-eslint/visitor-keys" "4.16.1" -"@typescript-eslint/types@4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.15.1.tgz#da702f544ef1afae4bc98da699eaecd49cf31c8c" - integrity sha512-iGsaUyWFyLz0mHfXhX4zO6P7O3sExQpBJ2dgXB0G5g/8PRVfBBsmQIc3r83ranEQTALLR3Vko/fnCIVqmH+mPw== +"@typescript-eslint/types@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.16.1.tgz#5ba2d3e38b1a67420d2487519e193163054d9c15" + integrity sha512-nnKqBwMgRlhzmJQF8tnFDZWfunXmJyuXj55xc8Kbfup4PbkzdoDXZvzN8//EiKR27J6vUSU8j4t37yUuYPiLqA== -"@typescript-eslint/typescript-estree@4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.15.1.tgz#fa9a9ff88b4a04d901ddbe5b248bc0a00cd610be" - integrity sha512-z8MN3CicTEumrWAEB2e2CcoZa3KP9+SMYLIA2aM49XW3cWIaiVSOAGq30ffR5XHxRirqE90fgLw3e6WmNx5uNw== +"@typescript-eslint/typescript-estree@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.16.1.tgz#c2fc46b05a48fbf8bbe8b66a63f0a9ba04b356f1" + integrity sha512-m8I/DKHa8YbeHt31T+UGd/l8Kwr0XCTCZL3H4HMvvLCT7HU9V7yYdinTOv1gf/zfqNeDcCgaFH2BMsS8x6NvJg== dependencies: - "@typescript-eslint/types" "4.15.1" - "@typescript-eslint/visitor-keys" "4.15.1" + "@typescript-eslint/types" "4.16.1" + "@typescript-eslint/visitor-keys" "4.16.1" debug "^4.1.1" globby "^11.0.1" is-glob "^4.0.1" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@4.15.1": - version "4.15.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.1.tgz#c76abbf2a3be8a70ed760f0e5756bf62de5865dd" - integrity sha512-tYzaTP9plooRJY8eNlpAewTOqtWW/4ff/5wBjNVaJ0S0wC4Gpq/zDVRTJa5bq2v1pCNQ08xxMCndcvR+h7lMww== +"@typescript-eslint/visitor-keys@4.16.1": + version "4.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.16.1.tgz#d7571fb580749fae621520deeb134370bbfc7293" + integrity sha512-s/aIP1XcMkEqCNcPQtl60ogUYjSM8FU2mq1O7y5cFf3Xcob1z1iXWNB6cC43Op+NGRTFgGolri6s8z/efA9i1w== dependencies: - "@typescript-eslint/types" "4.15.1" + "@typescript-eslint/types" "4.16.1" eslint-visitor-keys "^2.0.0" "@yarnpkg/lockfile@^1.1.0": @@ -2265,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.848.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.848.0.tgz#5e7706ddd30a55a2d5a5b64c29682a757607ee64" - integrity sha512-c/e5kaEFl+9aYkrYDkmu5mSZlL+EfP6DnBOMD06fH12gIsaFSMBGtbsDTHABhvSu++LxeI1dJAD148O17MuZvg== + 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" @@ -2393,9 +2413,9 @@ before-after-hook@^2.0.0, before-after-hook@^2.1.0: integrity sha512-5ekuQOvO04MDj7kYZJaMab2S8SPjGJbotVNyv7QYFCOAwrGZs/YnoDNlh1U+m5hl7H2D/+n0taaAV/tfyd3KMA== bind-obj-methods@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/bind-obj-methods/-/bind-obj-methods-2.0.0.tgz#0178140dbe7b7bb67dc74892ace59bc0247f06f0" - integrity sha512-3/qRXczDi2Cdbz6jE+W3IflJOutRVica8frpBn14de1mBOkzDo+6tY33kNhvkw54Kn3PzRRD2VnGbGPcTAk4sw== + version "2.0.1" + resolved "https://registry.yarnpkg.com/bind-obj-methods/-/bind-obj-methods-2.0.1.tgz#1c1295d6741c07b78d15f42080fe4a60a27f91f5" + integrity sha512-kKzUyCuc+jsWH4C2nW5KB2nh+rQRbQcdphfo9UN3j1uwIFGZ3JB8njtRZOiUAQCkxazH0nDQPN6x/zhvFcbZIw== bl@^4.0.3: version "4.1.0" @@ -2447,6 +2467,17 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== +browserslist@^4.14.5: + version "4.16.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" + integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== + dependencies: + caniuse-lite "^1.0.30001181" + colorette "^1.2.1" + electron-to-chromium "^1.3.649" + escalade "^3.1.1" + node-releases "^1.1.70" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -2652,6 +2683,11 @@ camelcase@^6.0.0, camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== +caniuse-lite@^1.0.30001181: + 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" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -2832,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.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.21.0.tgz#3dd1c236b6af3d8ac90f99e300e56e80c799ea76" - integrity sha512-YxTt3lWcR6PC/3fByU7FGjIoUDOcTs1KmqRJcK14xN9X7wxBOWO129WuSTm/4XfKlz/3iSo9CtRX/5HYkE1oCQ== +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" @@ -2883,6 +2919,11 @@ color-support@^1.1.0: resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== +colorette@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" + integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== + colors@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" @@ -2903,10 +2944,10 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@~6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" - integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== +commander@~7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff" + integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== commondir@^1.0.1: version "1.0.1" @@ -2936,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" @@ -2985,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.29" - resolved "https://registry.yarnpkg.com/constructs/-/constructs-3.3.29.tgz#42d2fbc0d872a47701a70583a0d4356a3f11955c" - integrity sha512-rGQzkq2M/qKZ0hMEtt4YPpsZKOwzmiyAQx3PqexXXsjdVnTqEfIwQuDpc+1jP6CtaBHl7rR6CxQcfsP5DmaERw== + 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" @@ -3740,6 +3781,11 @@ ejs@^2.5.2: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== +electron-to-chromium@^1.3.649: + 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" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" @@ -3776,11 +3822,16 @@ enquirer@^2.3.5: dependencies: ansi-colors "^4.1.1" -entities@~2.0, entities@~2.0.0: +entities@~2.0: version "2.0.3" resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ== +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + env-paths@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" @@ -3804,24 +3855,26 @@ 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.2" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.2.tgz#088101a55f0541f595e7e057199e27ddc8f3a5c2" - integrity sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw== + 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" function-bind "^1.1.1" - get-intrinsic "^1.0.2" + get-intrinsic "^1.1.1" has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" + has-symbols "^1.0.2" + is-callable "^1.2.3" is-negative-zero "^2.0.1" - is-regex "^1.1.1" + is-regex "^1.1.2" + is-string "^1.0.5" object-inspect "^1.9.0" object-keys "^1.1.1" object.assign "^4.1.2" - string.prototype.trimend "^1.0.3" - string.prototype.trimstart "^1.0.3" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.0" es-get-iterator@^1.1.1: version "1.1.2" @@ -3868,10 +3921,10 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -esbuild@^0.8.50: - version "0.8.50" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.8.50.tgz#ebf24fde0cdad1a369789dd6fd7a820b0a01e46c" - integrity sha512-oidFLXssA7IccYzkqLVZSqNJDwDq8Mh/vqvrW+3fPWM7iUiC5O2bCllhnO8+K9LlyL/2Z6n+WwRJAz9fqSIVRg== +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" @@ -4023,13 +4076,13 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint@^7.20.0: - version "7.20.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.20.0.tgz#db07c4ca4eda2e2316e7aa57ac7fc91ec550bdc7" - integrity sha512-qGi0CTcOGP2OtCQBgWZlQjcTuP0XkIpYFj25XtRTQSHC+umNnp7UMshr2G8SLsRFYDdAPFeHOsiteadmMH02Yw== +eslint@^7.21.0: + version "7.21.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.21.0.tgz#4ecd5b8c5b44f5dedc9b8a110b01bbfeb15d1c83" + integrity sha512-W2aJbXpMNofUp0ztQaF40fveSsJBjlSCSWpy//gzfTvwC+USs/nceBrKmlJOiM8r1bLwP2EuYkCqArn/6QTIgg== dependencies: "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.3.0" + "@eslint/eslintrc" "^0.4.0" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" @@ -4042,7 +4095,7 @@ eslint@^7.20.0: espree "^7.3.1" esquery "^1.4.0" esutils "^2.0.2" - file-entry-cache "^6.0.0" + file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" glob-parent "^5.0.0" globals "^12.1.0" @@ -4314,9 +4367,9 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= fastq@^1.6.0: - version "1.10.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.10.1.tgz#8b8f2ac8bf3632d67afcd65dac248d5fdc45385e" - integrity sha512-AWuv6Ery3pM+dY7LYS8YIaCiQvUaos9OB1RyNgaOWnaX+Tik7Onvcsf8x8c+YtDeT0maYLniBip2hox5KtEXXA== + version "1.11.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" + integrity sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g== dependencies: reusify "^1.0.4" @@ -4346,7 +4399,7 @@ figures@^3.1.0: dependencies: escape-string-regexp "^1.0.5" -file-entry-cache@^6.0.0: +file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== @@ -4471,9 +4524,9 @@ flush-write-stream@^1.0.0: readable-stream "^2.3.6" follow-redirects@^1.10.0, follow-redirects@^1.11.0: - version "1.13.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.2.tgz#dd73c8effc12728ba5cf4259d760ea5fb83e3147" - integrity sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA== + version "1.13.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267" + integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA== for-in@^1.0.2: version "1.0.2" @@ -4640,7 +4693,7 @@ genfun@^5.0.0: resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA== -gensync@^1.0.0-beta.1: +gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== @@ -4916,6 +4969,11 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== +has-bigints@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -4926,10 +4984,10 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-symbols@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" - integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== +has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== has-unicode@^2.0.0, has-unicode@^2.0.1: version "2.0.1" @@ -5307,7 +5365,7 @@ is-buffer@^1.1.5, is-buffer@~1.1.6: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-callable@^1.1.4, is-callable@^1.2.2: +is-callable@^1.1.4, is-callable@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== @@ -5495,7 +5553,7 @@ is-potential-custom-element-name@^1.0.0: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= -is-regex@^1.1.1: +is-regex@^1.1.1, is-regex@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== @@ -6135,7 +6193,7 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1, js-yaml@^3.2.7, js-yaml@~3.14.1: +js-yaml@^3.13.1, js-yaml@^3.2.7: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -6143,6 +6201,13 @@ js-yaml@^3.13.1, js-yaml@^3.2.7, js-yaml@~3.14.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" + integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== + dependencies: + argparse "^2.0.1" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -6185,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.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.21.0.tgz#4e7f5b38fa42b4ee109592686ec5d5b168491c0b" - integrity sha512-88YUb9FO2jb6We9fTowR2k1+YhnFJ1LYDOOw7ThyyNyjsgEAheOV5vB4u43HTduZncVmncUNjcMirrUDm2JDzA== +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.21.0" + "@jsii/spec" "^1.24.0" fs-extra "^9.1.0" - jsii-reflect "^1.21.0" + jsii-reflect "^1.24.0" log4js "^6.3.0" typescript "~3.9.9" yargs "^16.2.0" -jsii-pacmak@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.21.0.tgz#b9da4b5d2e980bc75ec37336175a2b99752d677f" - integrity sha512-04/fIZqM31cfTf48v7ni7MGeAwBEREP1WhvGkf4TSAZmAdMx1FUWQxuKiDK1/YeEvIUhNHIy/Ng9GcoSf+Rwfg== +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.21.0" + "@jsii/spec" "^1.24.0" clone "^2.1.2" - codemaker "^1.21.0" + codemaker "^1.24.0" commonmark "^0.29.3" escape-string-regexp "^4.0.0" fs-extra "^9.1.0" - jsii-reflect "^1.21.0" - jsii-rosetta "^1.21.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.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.21.0.tgz#2f6f7835d7428c5cc9d2c81f1c881e613710bc78" - integrity sha512-OwXhVhe+NRv/e6jaGBdIpm3S1KQcEXTZN+USiBd+c4kROLqxw+ubpMBsEVSKEZ7t+4WksLTWWNot31VZkJrZ5g== +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.21.0" + "@jsii/spec" "^1.24.0" colors "^1.4.0" fs-extra "^9.1.0" - oo-ascii-tree "^1.21.0" + oo-ascii-tree "^1.24.0" yargs "^16.2.0" -jsii-rosetta@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.21.0.tgz#591ebb2ce390b81f269a66d906446f8bebe9e23b" - integrity sha512-8W0vcWTr28q+1NWhVAY4lOwOlPHdGdg8b/gPHFccRi9ZM4uwRjW7YjmqD9FmX74dEg1Qmvd8nujW4Opow6PFtQ== +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.21.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.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.21.0.tgz#6fd5dd9a18bb820a127ab51f55b2081cf14b2181" - integrity sha512-6siaRt1OyrQxC9pzLaFGj6bDkHMTsofcu8ODM0NCcukq2P4PlF1O39H0DV8Z40QF3KWbawJ/Utl7GtaSrdG2Ww== +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.21.0" + "@jsii/spec" "^1.24.0" case "^1.6.3" colors "^1.4.0" deep-equal "^2.0.5" @@ -6804,48 +6869,48 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -markdown-it@12.0.2: - version "12.0.2" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.2.tgz#4401beae8df8aa2221fc6565a7188e60a06ef0ed" - integrity sha512-4Lkvjbv2kK+moL9TbeV+6/NHx+1Q+R/NIdUlFlkqkkzUcTod4uiyTJRiBidKR9qXSdkNFkgv+AELY8KN9vSgVA== +markdown-it@12.0.4: + version "12.0.4" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.0.4.tgz#eec8247d296327eac3ba9746bdeec9cfcc751e33" + integrity sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q== dependencies: argparse "^2.0.1" - entities "~2.0.0" + entities "~2.1.0" linkify-it "^3.0.1" mdurl "^1.0.1" uc.micro "^1.0.5" -markdownlint-cli@^0.26.0: - version "0.26.0" - resolved "https://registry.yarnpkg.com/markdownlint-cli/-/markdownlint-cli-0.26.0.tgz#cd89e3e39a049303ec125c8aa291da4f3325df29" - integrity sha512-biLfeGNZG9nw0yJbtFBzRlew2/P5w7JSseKwolSox3zejs7dLpGvPgqbC+iqJnqqGWcWLtXaXh8bBEKWmfl10A== +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 "~6.2.1" + commander "~7.1.0" deep-extend "~0.6.0" get-stdin "~8.0.0" glob "~7.1.6" ignore "~5.1.8" - js-yaml "~3.14.1" + js-yaml "^4.0.0" jsonc-parser "~3.0.0" lodash.differencewith "~4.5.0" lodash.flatten "~4.4.0" - markdownlint "~0.22.0" - markdownlint-rule-helpers "~0.13.0" + markdownlint "~0.23.1" + markdownlint-rule-helpers "~0.14.0" minimatch "~3.0.4" minimist "~1.2.5" rc "~1.2.8" -markdownlint-rule-helpers@~0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.13.0.tgz#7cc6553bc7f8c4c8a43cf66fb2a3a652124f46f9" - integrity sha512-rRY0itbcHG4e+ntz0bbY3AIceSJMKS0TafEMgEtKVHRZ54/JUSy6/4ypCL618RlJvYRej+xMLxX5nkJqIeTZaQ== +markdownlint-rule-helpers@~0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.14.0.tgz#4d0e1ae320e85559d8cbed1490934855791627bb" + integrity sha512-vRTPqSU4JK8vVXmjICHSBhwXUvbfh/VJo+j7hvxqe15tLJyomv3FLgFdFgb8kpj0Fe8SsJa/TZUAXv7/sN+N7A== -markdownlint@~0.22.0: - version "0.22.0" - resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.22.0.tgz#4ed95b61c17ae9f4dfca6a01f038c744846c0a72" - integrity sha512-J4B+iMc12pOdp/wfYi03W2qfAfEyiZzq3qvQh/8vOMNU8vXYY6Jg440EY7dWTBCqROhb1i4nAn3BTByJ5kdx1w== +markdownlint@~0.23.1: + version "0.23.1" + resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.23.1.tgz#98292b5d340d01e9c113f3d7fb3b2ccf89628dc2" + integrity sha512-iOEwhDfNmq2IJlaA8mzEkHYUi/Hwoa6Ss+HO5jkwUR6wQ4quFr0WzSx+Z9rsWZKUaPbyirIdL1zGmJRkWawr4Q== dependencies: - markdown-it "12.0.2" + markdown-it "12.0.4" md5@^2.3.0: version "2.3.0" @@ -7208,10 +7273,10 @@ nise@^4.0.4: just-extend "^4.0.2" path-to-regexp "^1.7.0" -nock@^13.0.7: - version "13.0.7" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.7.tgz#9bc718c66bd0862dfa14601a9ba678a406127910" - integrity sha512-WBz73VYIjdbO6BwmXODRQLtn7B5tldA9pNpWJe5QTtTEscQlY5KXU4srnGzBOK2fWakkXj69gfTnXGzmrsaRWw== +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" @@ -7278,6 +7343,11 @@ node-preload@^0.2.1: dependencies: process-on-spawn "^1.0.0" +node-releases@^1.1.70: + version "1.1.71" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" + integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== + nodeunit@^0.11.3: version "0.11.3" resolved "https://registry.yarnpkg.com/nodeunit/-/nodeunit-0.11.3.tgz#313afae26cd11b407b731ff774b8e35e5d6f9568" @@ -7559,13 +7629,13 @@ object.pick@^1.3.0: isobject "^3.0.1" object.values@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.2.tgz#7a2015e06fcb0f546bd652486ce8583a4731c731" - integrity sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag== + version "1.1.3" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.3.tgz#eaa8b1e17589f02f698db093f7c62ee1699742ee" + integrity sha512-nkF6PfDB9alkOUxpf1HNm/QlkeW3SReqL5WXeBLpEJJnlPSvRaDQpW3gQTksTN3fgJX4hL42RzKyOin6ff3tyw== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" + es-abstract "^1.18.0-next.2" has "^1.0.3" octokit-pagination-methods@^1.1.0: @@ -7594,10 +7664,10 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -oo-ascii-tree@^1.21.0: - version "1.21.0" - resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.21.0.tgz#8408fceb90799c7af769b7ab709ec35ea9868b8b" - integrity sha512-N91VyM/R9K8axskaVYSg+IJiSDJVKFQ2IfQyBp5Rv7t2YETjJDMgA6Ew9MGv82fhpz95qKLlZmgrQsb7scb2Eg== +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" @@ -8243,9 +8313,9 @@ qs@~6.5.2: integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== query-string@^6.13.8: - version "6.14.0" - resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.0.tgz#0b7b7ca326f5facf10dd2d45d26645cd287f8c92" - integrity sha512-In3o+lUxlgejoVJgwEdYtdxrmlL0cQWJXj0+kkI7RWVo7hg5AhFtybeKlC9Dpgbr8eOC4ydpEh8017WwyfzqVQ== + version "6.14.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" + integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== dependencies: decode-uri-component "^0.2.0" filter-obj "^1.1.0" @@ -8704,9 +8774,9 @@ run-queue@^1.0.0, run-queue@^1.0.3: aproba "^1.1.1" rxjs@^6.4.0: - version "6.6.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" - integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== + version "6.6.6" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.6.tgz#14d8417aa5a07c5e633995b525e1e3c0dec03b70" + integrity sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg== dependencies: tslib "^1.9.0" @@ -9239,10 +9309,10 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-width@*, string-width@^4.1.0, string-width@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" - integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== +string-width@*, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" + integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" @@ -9279,20 +9349,20 @@ string.prototype.repeat@^0.2.0: resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-0.2.0.tgz#aba36de08dcee6a5a337d49b2ea1da1b28fc0ecf" integrity sha1-q6Nt4I3O5qWjN9SbLqHaGyj8Ds8= -string.prototype.trimend@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz#a22bd53cca5c7cf44d7c9d5c732118873d6cd18b" - integrity sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw== +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" -string.prototype.trimstart@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz#9b4cb590e123bb36564401d59824298de50fd5aa" - integrity sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg== +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== dependencies: - call-bind "^1.0.0" + call-bind "^1.0.2" define-properties "^1.1.3" string_decoder@^1.1.1: @@ -9793,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.1: - version "26.5.1" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.1.tgz#4d53ee4481552f57c1624f0bd3425c8b17996150" - integrity sha512-G7Rmo3OJMvlqE79amJX8VJKDiRcd7/r61wh9fnvvG8cAjhA9edklGw/dCxRSQmfZ/z8NDums5srSVgwZos1qfg== +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" @@ -9956,9 +10025,9 @@ typescript@^3.3.3, typescript@~3.9.9: integrity sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w== typescript@^4.1.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" - integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== + version "4.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.2.tgz#1450f020618f872db0ea17317d16d8da8ddb8c4c" + integrity sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ== typescript@~3.8.3: version "3.8.3" @@ -9971,9 +10040,9 @@ uc.micro@^1.0.1, uc.micro@^1.0.5: integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== uglify-js@^3.1.4: - version "3.12.8" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.12.8.tgz#a82e6e53c9be14f7382de3d068ef1e26e7d4aaf8" - integrity sha512-fvBeuXOsvqjecUtF/l1dwsrrf5y2BCUk9AOJGzGcm6tE7vegku5u/YvqjyDaAGr422PLoLnrxg3EnRvTqsdC1w== + version "3.13.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.0.tgz#66ed69f7241f33f13531d3d51d5bcebf00df7f69" + integrity sha512-TWYSWa9T2pPN4DIJYbU9oAjQx+5qdV5RUDxwARg8fmJZrD/V27Zj0JngW5xg1DFz42G0uDYl2XhzF6alSzD62w== uid-number@0.0.6: version "0.0.6" @@ -9985,6 +10054,16 @@ umask@^1.1.0: resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0= +unbox-primitive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.0.tgz#eeacbc4affa28e9b3d36b5eaeccc50b3251b1d3f" + integrity sha512-P/51NX+JXyxK/aigg1/ZgyccdAxm5K1+n8+tvqSntjOivPt19gvm1VC49RWYetsiub8WViUchdxl/KWHHB0kzA== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.0" + has-symbols "^1.0.0" + which-boxed-primitive "^1.0.1" + unicode-length@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/unicode-length/-/unicode-length-2.0.2.tgz#e5eb4c0d523fdf7bebb59ca261c9ca1cf732da96" @@ -10593,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"