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/general-issues.md b/.github/ISSUE_TEMPLATE/general-issues.md index 8ed1e4209a644..edd2ef2798236 100644 --- a/.github/ISSUE_TEMPLATE/general-issues.md +++ b/.github/ISSUE_TEMPLATE/general-issues.md @@ -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/.gitignore b/.gitignore index f8f8e687c6791..2cb016405e016 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # VSCode extension -.vscode/ + +# Store launch config in repo but not settings +.vscode/settings.json /.favorites.json # TypeScript incremental build states 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/CHANGELOG.md b/CHANGELOG.md index d060fb6f61a28..797f21440a6fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,62 @@ 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.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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad4328ebde45a..810267a9ab428 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,6 +43,7 @@ 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 @@ -234,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 @@ -327,7 +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). +extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). #### pkglint @@ -910,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 ba7a422bb528f..9b51cbaba4118 100644 --- a/README.md +++ b/README.md @@ -118,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 8b137891791fe..e6bdc57ed11ae 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -1 +1,5 @@ +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 b533a6ac4d33c..9e6a0c4ba7b18 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.42.0" + "version": "1.44.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 6a465930f912c..199c117aa6641 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,11 @@ }, "devDependencies": { "conventional-changelog-cli": "^2.0.34", - "fs-extra": "^8.1.0", - "jsii-diff": "^1.5.0", - "jsii-pacmak": "^1.5.0", - "jsii-rosetta": "^1.5.0", - "lerna": "^3.21.0", + "fs-extra": "^9.0.1", + "jsii-diff": "^1.6.0", + "jsii-pacmak": "^1.6.0", + "jsii-rosetta": "^1.6.0", + "lerna": "^3.22.0", "standard-version": "^8.0.0", "graceful-fs": "^4.2.4", "typescript": "~3.8.3" diff --git a/packages/@aws-cdk/assert/package.json b/packages/@aws-cdk/assert/package.json index bab4a3d1dd077..1ad7d397c5c6f 100644 --- a/packages/@aws-cdk/assert/package.json +++ b/packages/@aws-cdk/assert/package.json @@ -25,7 +25,7 @@ "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^26.0.0" + "ts-jest": "^26.1.0" }, "dependencies": { "@aws-cdk/cloudformation-diff": "0.0.0", diff --git a/packages/@aws-cdk/assets/package.json b/packages/@aws-cdk/assets/package.json index 7892cc9fc6801..92cae774b8353 100644 --- a/packages/@aws-cdk/assets/package.json +++ b/packages/@aws-cdk/assets/package.json @@ -65,7 +65,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "@types/sinon": "^9.0.3", + "@types/sinon": "^9.0.4", "aws-cdk": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", 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/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/integration.ts b/packages/@aws-cdk/aws-apigateway/lib/integration.ts index 05356a57a861e..d7a9ec74f3b34 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integration.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integration.ts @@ -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/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 index 71dd02f17ab9a..bcf74c12601fa 100644 --- 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 @@ -44,7 +44,7 @@ "Name": "my-api" } }, - "myapiDeployment92F2CB49": { + "myapiDeployment92F2CB49eb6b0027bfbdb20b09988607569e06bd": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -60,7 +60,7 @@ "Ref": "myapi4C7BF186" }, "DeploymentId": { - "Ref": "myapiDeployment92F2CB49" + "Ref": "myapiDeployment92F2CB49eb6b0027bfbdb20b09988607569e06bd" }, "StageName": "prod" } 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 index 3eaae1ff8fd58..e319d4fb28ccd 100644 --- 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 @@ -53,7 +53,7 @@ "Name": "my-api" } }, - "myapiDeployment92F2CB49": { + "myapiDeployment92F2CB49a59bca458e4fac1fcd742212ded42a65": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { @@ -69,7 +69,7 @@ "Ref": "myapi4C7BF186" }, "DeploymentId": { - "Ref": "myapiDeployment92F2CB49" + "Ref": "myapiDeployment92F2CB49a59bca458e4fac1fcd742212ded42a65" }, "StageName": "prod" } 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.restapi.books.expected.json b/packages/@aws-cdk/aws-apigateway/test/integ.restapi.books.expected.json index 0d471973c58ca..8b679bd6c6239 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": { 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 21b7c8298d601..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 @@ -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-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 3bf41e5ca126d..2384382bc90b2 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.681.0", + "aws-sdk": "^2.689.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudtrail/README.md b/packages/@aws-cdk/aws-cloudtrail/README.md index a1619d4bf48fe..541c926cab6fb 100644 --- a/packages/@aws-cdk/aws-cloudtrail/README.md +++ b/packages/@aws-cdk/aws-cloudtrail/README.md @@ -13,101 +13,82 @@ --- -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 - -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; - const trail = new cloudtrail.Trail(this, 'CloudTrail', { - sendToCloudWatchLogs: true + enableFileValidation: false, }); ``` -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 -via the `cloudWatchLogsRetention` property. If you would like to specify the log group explicitly, -use the `cloudwatchLogGroup` property. +## Notifications -For using CloudTrail event selector to log specific S3 events, -you can use the `CloudTrailProps` configuration object. -Example: +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 -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; +const topic = new sns.Topic(this, 'TrailTopic'); +const trail = new cloudtrail.Trail(this, 'CloudTrail', { + snsTopic: topic, +}); +``` -const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); +## Service Integrations -// Adds an event selector to the bucket magic-bucket. -// By default, this includes management events and all operations (Read + Write) -trail.logAllS3DataEvents(); +Besides sending trail events to S3, they can also be configured to notify other AWS services - -// Adds an event selector to the bucket foo -trail.addS3EventSelector([{ - bucket: fooBucket // 'fooBucket' is of type s3.IBucket -}]); -``` +### Amazon CloudWatch Logs -For using CloudTrail event selector to log events about Lambda -functions, you can use `addLambdaEventSelector`. +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'; -import * as lambda from '@aws-cdk/aws-lambda'; - -const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); -const lambdaFunction = new lambda.Function(stack, 'AnAmazingFunction', { - runtime: lambda.Runtime.NODEJS_10_X, - handler: "hello.handler", - code: lambda.Code.fromAsset("lambda"), +const trail = new cloudtrail.Trail(this, 'CloudTrail', { + sendToCloudWatchLogs: true, + cloudWatchLogsRetention: logs.RetentionDays.FOUR_MONTHS, }); +``` -// Add an event selector to log data events for all functions in the account. -trail.logAllLambdaDataEvents(); +If you would like to use a specific log group instead, this can be configured via `cloudwatchLogGroup`. -// Add an event selector to log data events for the provided Lambda functions. -trail.addLambdaEventSelector([lambdaFunction.functionArn]); -``` +### Amazon EventBridge -Using the `Trail.onEvent()` API, an EventBridge rule can be created that gets triggered for -every event logged in CloudTrail. -To only use the events that are of interest, either from a particular service, specific account or -time range, they can be filtered down using the APIs available in `aws-events`. The following code -filters events for S3 from a specific AWS account and triggers a lambda function. See [Events delivered via +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. -```ts -import * as cloudtrail from '@aws-cdk/aws-cloudtrail'; -import * as eventTargets from '@aws-cdk/aws-events-targets'; -import * as lambda from '@aws-cdk/aws-lambda'; +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, @@ -123,3 +104,84 @@ eventRule.addEventPattern({ source: 'aws.s3', }); ``` + +## 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'; + +const trail = new cloudtrail.Trail(this, 'MyAmazingCloudTrail'); + +// 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 the provided Lambda functions. +trail.addLambdaEventSelector([ lambdaFunction ]); +``` \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts index 9c38e6ca06814..3b3f39d64eb4c 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts @@ -41,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; @@ -131,7 +131,12 @@ export enum ReadWriteType { /** * All events */ - ALL = 'All' + ALL = 'All', + + /** + * No events + */ + NONE = 'None', } /** @@ -235,10 +240,17 @@ export class Trail extends Resource { } 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); } diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index cc2107d9dabab..2ed0d52f9378a 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -64,7 +64,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "aws-sdk": "^2.681.0", + "aws-sdk": "^2.689.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts index 7137a1ea4a7f0..50c2b766bb4c3 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts @@ -397,6 +397,22 @@ describe('cloudtrail', () => { ], }); }); + + 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, + }, + ], + }); + }); }); }); diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index 40bc3b600f959..f380c06de5364 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -70,7 +70,7 @@ "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.681.0", + "aws-sdk": "^2.689.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 4d1a07638ef0d..3290ef3d3f408 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -70,7 +70,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.681.0", + "aws-sdk": "^2.689.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", 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/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index af13cef9e8ade..65c3d3b886f1f 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", - "@types/lodash": "^4.14.152", + "@types/lodash": "^4.14.155", "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/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/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 26c86887e98bc..b498c20945f83 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -749,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; } @@ -874,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/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-cognito/README.md b/packages/@aws-cdk/aws-cognito/README.md index 192fd76826e64..8ee0f57c7db5a 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) @@ -334,6 +335,36 @@ 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, +}); +``` + +In order to allow users to sign in with a third-party identity provider, the app client that faces the user should be +configured to use the identity provider. See [App Clients](#app-clients) section to know more about App Clients. +The identity providers should be configured on `identityProviders` property available on the `UserPoolClient` construct. + ### App Clients An app is an entity within a user pool that has permission to call unauthenticated APIs (APIs that do not have an @@ -417,6 +448,22 @@ pool.addClient('app-client', { }); ``` +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 @@ -447,3 +494,31 @@ 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). + +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/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/user-pool-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts index e2a76c64120ef..c6fde417d1e4e 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -1,3 +1,5 @@ +import { Token } from '@aws-cdk/core'; + /** * The set of standard attributes that can be marked as required. * @@ -200,10 +202,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 039c17376b8fe..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 */ @@ -182,6 +219,15 @@ export interface UserPoolClientOptions { * @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[]; } /** @@ -221,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; /* @@ -234,16 +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; @@ -275,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; @@ -296,16 +355,15 @@ 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); } - if (oAuthScopes.size > 0) { - return Array.from(oAuthScopes); - } - return undefined; + return Array.from(scopeNames); } private configurePreventUserExistenceErrors(prevent?: boolean): string | undefined { @@ -314,4 +372,17 @@ export class UserPoolClient extends Resource implements IUserPoolClient { } 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); + } + 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..d5f4fd5402609 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts @@ -0,0 +1,52 @@ +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(' '), + }, + }); + + 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..b95ffd106a285 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts @@ -0,0 +1,25 @@ +import { Construct, Resource } from '@aws-cdk/core'; +import { IUserPool } from '../user-pool'; +import { IUserPoolIdentityProvider } from '../user-pool-idp'; + +/** + * Properties to create a new instance of UserPoolIdentityProvider + */ +export interface UserPoolIdentityProviderProps { + /** + * The user pool to which this construct provides identities. + */ + readonly userPool: IUserPool; +} + +/** + * 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, props: UserPoolIdentityProviderProps) { + super(scope, id); + props.userPool.registerIdentityProvider(this); + } +} \ 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..d404c40965575 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts @@ -0,0 +1,57 @@ +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, + }, + }); + + 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 a534eefbc509a..a0f25c13cd58b 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -1,10 +1,11 @@ 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 { 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. @@ -525,24 +526,35 @@ export interface IUserPool extends IResource { */ readonly userPoolArn: string; + /** + * 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): IUserPoolClient; + 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 */ 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): IUserPoolClient { + public addClient(id: string, options?: UserPoolClientOptions): UserPoolClient { return new UserPoolClient(this, id, { userPool: this, ...options, @@ -555,6 +567,10 @@ abstract class UserPoolBase extends Resource implements IUserPool { ...options, }); } + + public registerIdentityProvider(provider: IUserPoolIdentityProvider) { + this.identityProviders.push(provider); + } } /** @@ -706,10 +722,10 @@ export class UserPool extends UserPoolBase { if (emailStyle === VerificationEmailStyle.CODE) { const emailMessage = props.userVerification?.emailBody ?? `The verification code to your new account is ${CODE_TEMPLATE}`; - if (emailMessage.indexOf(CODE_TEMPLATE) < 0) { + 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 { @@ -721,7 +737,7 @@ export class UserPool extends UserPoolBase { } else { const emailMessage = props.userVerification?.emailBody ?? `Verify your account by clicking on ${VERIFY_EMAIL_TEMPLATE}`; - if (emailMessage.indexOf(VERIFY_EMAIL_TEMPLATE) < 0) { + 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 { diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index d3f83d76fcb5c..fee82c6b6c883 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -96,7 +96,9 @@ "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 63556451e98ff..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 @@ -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" @@ -94,8 +93,11 @@ "ALLOW_REFRESH_TOKEN_AUTH" ], "GenerateSecret": true, - "PreventUserExistenceErrors": "ENABLED" + "PreventUserExistenceErrors": "ENABLED", + "SupportedIdentityProviders": [ + "COGNITO" + ] } } } -} +} \ No newline at end of file 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 92a8bd8f19321..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,7 +27,6 @@ userpool.addClient('myuserpoolclient', { OAuthScope.OPENID, OAuthScope.PROFILE, OAuthScope.COGNITO_ADMIN, - OAuthScope.custom('my-resource-server/my-scope'), ], callbackUrls: [ 'https://redirect-here.myapp.com' ], }, 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-idp.expected.json b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json new file mode 100644 index 0000000000000..bbed1eca96f4c --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.expected.json @@ -0,0 +1,143 @@ +{ + "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" + }, + "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..e22b504cf8ad7 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/integ.user-pool-idp.ts @@ -0,0 +1,32 @@ +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { 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', +}); + +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/user-pool-attr.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-attr.test.ts index f001712a802a7..212f6835cb508 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', () => { 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 d1e0862df0a50..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/); @@ -336,4 +365,48 @@ describe('User Pool Client', () => { 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..78300c6b13e5f --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts @@ -0,0 +1,70 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { 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); + }); + }); +}); \ 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..40bc9287b5733 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts @@ -0,0 +1,72 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { 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); + }); + }); +}); \ 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 83d4863b751c3..9fad806f888ad 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', () => { @@ -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(); @@ -847,6 +866,21 @@ test('addDomain', () => { }); }); +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, diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index f63ff13a25cf4..1c1802f039153 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -412,7 +412,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 */ @@ -420,7 +420,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 */ @@ -439,6 +439,28 @@ export interface TableAttributes { * @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 { @@ -682,7 +704,7 @@ abstract class TableBase extends Resource implements ITable { 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 }), @@ -773,6 +795,8 @@ export class Table extends TableBase { 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); @@ -781,10 +805,6 @@ export class Table extends TableBase { this.tableStreamArn = tableStreamArn; this.encryptionKey = attrs.encryptionKey; } - - protected get hasIndex(): boolean { - return false; - } } let name: string; diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index 77e7e4ee0d1e1..65075f253bcff 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -65,7 +65,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/jest": "^25.2.3", - "aws-sdk": "^2.681.0", + "aws-sdk": "^2.689.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", @@ -73,7 +73,7 @@ "jest": "^25.5.4", "pkglint": "0.0.0", "sinon": "^9.0.2", - "ts-jest": "^26.0.0" + "ts-jest": "^26.1.0" }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index 068cfaf5b0edb..c0c0fe9633ac0 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -2182,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/*', + ]], + }, + ], + }, + ], + }, + }); + }); }); }); 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/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/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/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/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/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-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index aa0b114f5de47..2aa311168dc6a 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.681.0", + "aws-sdk": "^2.689.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", 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 c087f690f70e5..f671e705ec78b 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -244,6 +244,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, diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.actions.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.actions.ts index 1921303469968..62a1c378bf067 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.actions.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.actions.ts @@ -182,4 +182,53 @@ export = { 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/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-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/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 0216eabf50638..4bdff4663018d 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -68,7 +68,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", - "aws-sdk": "^2.681.0", + "aws-sdk": "^2.689.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", @@ -88,6 +88,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" }, @@ -106,7 +107,8 @@ "@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 <13 || >=13.7.0" 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-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/lib/builder.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts index dd8e3ba2f8565..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,6 +118,7 @@ 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}`] : []), + ...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', ]; @@ -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 b932a81975ee0..b415c57d92d9e 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/package.json +++ b/packages/@aws-cdk/aws-lambda-nodejs/package.json @@ -62,7 +62,7 @@ "@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": { 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 6c7f5e41ae3e0..55502d783ec26 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts @@ -80,6 +80,30 @@ test('with Windows paths', () => { ])); }); +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/lib/function-base.ts b/packages/@aws-cdk/aws-lambda/lib/function-base.ts index 65a6fe6c05c26..b9a2e6b4ef166 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function-base.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function-base.ts @@ -284,7 +284,7 @@ export abstract class FunctionBase extends Resource implements IFunction { action: 'lambda:InvokeFunction', }); - return { statementAdded: true, policyDependable: this.node.findChild(identifier) } as iam.AddToResourcePolicyResult; + return { statementAdded: true, policyDependable: this._functionNode().findChild(identifier) } as iam.AddToResourcePolicyResult; }, node: this.node, }, @@ -318,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/singleton-lambda.ts b/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts index d9bf1f97372a0..f8515dc84e841 100644 --- a/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/singleton-lambda.ts @@ -81,6 +81,14 @@ export class SingletonFunction extends FunctionBase { 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 292d921efb0fa..e6bcad26594e9 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.152", + "@types/lodash": "^4.14.155", "@types/nodeunit": "^0.0.31", - "@types/sinon": "^9.0.3", - "aws-sdk": "^2.681.0", + "@types/sinon": "^9.0.4", + "aws-sdk": "^2.689.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts index 5f815d8f5e237..05512ec54b2f0 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.singleton-lambda.ts @@ -113,4 +113,32 @@ export = { 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-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/test/integ.cluster-rotation.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json index 79fa1f3e2dab7..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 @@ -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 b9dc043a54b40..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 @@ -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 13642f995eeb1..37f63d001843e 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json @@ -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 ae832999de3b7..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 @@ -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-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index 0496e2094d9f9..c99f3de12f7a1 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.681.0", + "aws-sdk": "^2.689.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3-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/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/package.json b/packages/@aws-cdk/aws-s3-assets/package.json index ff1eb0933ce36..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.31", - "@types/sinon": "^9.0.3", - "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", @@ -93,9 +90,6 @@ }, "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/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/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/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/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-sam/package.json b/packages/@aws-cdk/aws-sam/package.json index 9f13bd373b5d0..e61c29fcf3d35 100644 --- a/packages/@aws-cdk/aws-sam/package.json +++ b/packages/@aws-cdk/aws-sam/package.json @@ -70,7 +70,7 @@ "cfn2ts": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^26.0.0" + "ts-jest": "^26.1.0" }, "dependencies": { "@aws-cdk/core": "0.0.0", 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.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index 4bb50b68aa684..b44c44206a0b3 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -135,7 +135,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, }); 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.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts index 3b043eb562a97..606bc33d9ec8b 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts @@ -189,7 +189,10 @@ export = { PolicyDocument: { Version: '2012-10-17', Statement: [{ - Action: 'secretsmanager:GetSecretValue', + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], Effect: 'Allow', Resource: { Ref: 'SecretA720EF05' }, }], @@ -252,7 +255,10 @@ export = { PolicyDocument: { Version: '2012-10-17', Statement: [{ - Action: 'secretsmanager:GetSecretValue', + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], Effect: 'Allow', Resource: { Ref: 'SecretA720EF05' }, Condition: { 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 84388336b33fa..ef28cad08a9ee 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -65,7 +65,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.681.0", + "aws-sdk": "^2.689.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", 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-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index c7cc3fe389099..c8482f9e57f09 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, }); ``` @@ -728,15 +727,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 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/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 7b45086a4e48e..b9a5cd0a9f062 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -14,6 +14,7 @@ export * from './sagemaker/sagemaker-task-base-types'; export * from './sagemaker/sagemaker-train-task'; export * from './sagemaker/sagemaker-transform-task'; 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'; @@ -25,4 +26,5 @@ 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/start-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts index 049e558971206..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; 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/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/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/cfnspec/package.json b/packages/@aws-cdk/cfnspec/package.json index c5eeee5a7b837..dc442ba4f5d9e 100644 --- a/packages/@aws-cdk/cfnspec/package.json +++ b/packages/@aws-cdk/cfnspec/package.json @@ -27,7 +27,7 @@ "@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/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index f54b98f3d193b..bb31931b64aac 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -36,7 +36,7 @@ "fast-check": "^1.24.2", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^26.0.0" + "ts-jest": "^26.1.0" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", diff --git a/packages/@aws-cdk/cloudformation-include/README.md b/packages/@aws-cdk/cloudformation-include/README.md index 9996c500feb7f..a64d7b988e9bb 100644 --- a/packages/@aws-cdk/cloudformation-include/README.md +++ b/packages/@aws-cdk/cloudformation-include/README.md @@ -88,6 +88,24 @@ 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, @@ -98,13 +116,13 @@ All items unchecked below are currently not supported. - [x] Resources - [ ] Parameters -- [ ] Conditions +- [x] Conditions - [ ] Outputs ### [Resource attributes](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-product-attribute-reference.html): - [x] Properties -- [ ] Condition +- [x] Condition - [x] DependsOn - [ ] CreationPolicy - [ ] UpdatePolicy @@ -119,7 +137,7 @@ All items unchecked below are currently not supported. - [x] Fn::Join - [x] Fn::If - [ ] Fn::And -- [ ] Fn::Equals +- [x] Fn::Equals - [ ] Fn::Not - [ ] Fn::Or - [ ] Fn::Base64 diff --git a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts index d825a7ae24845..9b1c21e5a590a 100644 --- a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts +++ b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts @@ -1,4 +1,5 @@ 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'; @@ -20,6 +21,7 @@ export interface CfnIncludeProps { * 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; @@ -33,7 +35,12 @@ export class CfnInclude extends core.CfnElement { // ToDo implement preserveLogicalIds=false this.preserveLogicalIds = true; - // instantiate all resources as CDK L1 objects + // 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); } @@ -63,14 +70,32 @@ export class CfnInclude extends core.CfnElement { 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 Resources, which will be taken care of by the created L1s - if (section !== 'Resources') { + // except Conditions and Resources, which will be taken care of by the created L1s + if (section !== 'Conditions' && section !== 'Resources') { ret[section] = this.template[section]; } } @@ -78,6 +103,18 @@ export class CfnInclude extends core.CfnElement { 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) { @@ -92,7 +129,7 @@ export class CfnInclude extends core.CfnElement { throw new Error(`Unrecognized CloudFormation resource type: '${resourceAttributes.Type}'`); } // fail early for resource attributes we don't support yet - const knownAttributes = ['Type', 'Properties', 'DependsOn', 'DeletionPolicy', 'UpdateReplacePolicy', 'Metadata']; + 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. ` + @@ -105,6 +142,10 @@ export class CfnInclude extends core.CfnElement { 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; diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index 5389145c4941a..bf3be1afd8d19 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -307,7 +307,7 @@ "cdk-build-tools": "0.0.0", "jest": "^25.4.0", "pkglint": "0.0.0", - "ts-jest": "^26.0.0" + "ts-jest": "^26.1.0" }, "bundledDependencies": [ "yaml" diff --git a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts index 3e03ed5468de4..038ea1e9e6dde 100644 --- a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts @@ -46,6 +46,12 @@ describe('CDK Include', () => { 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 { 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/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts index 121e163fb0ef4..0291de98eba95 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -232,6 +232,38 @@ describe('CDK Include', () => { }, 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'); @@ -244,12 +276,6 @@ describe('CDK Include', () => { }).toThrow(/Unsupported CloudFormation function 'Fn::Base64'/); }); - test('throws an exception when encountering the Condition attribute in a resource', () => { - expect(() => { - includeTestTemplate(stack, 'resource-attribute-condition.json'); - }).toThrow(/The Condition resource attribute is not supported by cloudformation-include yet/); - }); - test('throws an exception when encountering the CreationPolicy attribute in a resource', () => { expect(() => { includeTestTemplate(stack, 'resource-attribute-creation-policy.json'); diff --git a/packages/@aws-cdk/core/lib/cfn-parse.ts b/packages/@aws-cdk/core/lib/cfn-parse.ts index a72f59240776c..67e2d2390ef62 100644 --- a/packages/@aws-cdk/core/lib/cfn-parse.ts +++ b/packages/@aws-cdk/core/lib/cfn-parse.ts @@ -181,9 +181,16 @@ function parseIfCfnIntrinsic(object: any): any { } 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}'`); } 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/from-cfn.ts b/packages/@aws-cdk/core/lib/from-cfn.ts index 25127ad1fefbc..9d3b1544526a2 100644 --- a/packages/@aws-cdk/core/lib/from-cfn.ts +++ b/packages/@aws-cdk/core/lib/from-cfn.ts @@ -1,3 +1,4 @@ +import { CfnCondition } from './cfn-condition'; import { CfnResource } from './cfn-resource'; /** @@ -7,6 +8,13 @@ import { CfnResource } from './cfn-resource'; * @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, 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/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/default-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts index ace086a9c4bd3..5cef2ac3daab4 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts @@ -13,6 +13,11 @@ 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 */ @@ -44,7 +49,7 @@ export interface DefaultStackSynthesizerProps { readonly imageAssetsRepositoryName?: string; /** - * The role to use to publish assets to this environment + * 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. * @@ -52,16 +57,36 @@ export interface DefaultStackSynthesizerProps { * be replaced with the values of qualifier and the stack's account and region, * respectively. * - * @default DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN + * @default DefaultStackSynthesizer.DEFAULT_FILE_ASSET_PUBLISHING_ROLE_ARN */ - readonly assetPublishingRoleArn?: string; + readonly fileAssetPublishingRoleArn?: string; /** - * External ID to use when assuming role for asset publishing + * External ID to use when assuming role for file asset publishing * * @default - No external ID */ - readonly assetPublishingExternalId?: string; + 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 @@ -126,9 +151,14 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { 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. + * 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_ASSET_PUBLISHING_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-publishing-role-${AWS::AccountId}-${AWS::Region}'; + 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 @@ -145,7 +175,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { private repositoryName?: string; private _deployRoleArn?: string; private _cloudFormationExecutionRoleArn?: string; - private assetPublishingRoleArn?: string; + private fileAssetPublishingRoleArn?: string; + private imageAssetPublishingRoleArn?: string; private readonly files: NonNullable = {}; private readonly dockerImages: NonNullable = {}; @@ -178,7 +209,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { 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.assetPublishingRoleArn = specialize(this.props.assetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_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 } @@ -199,8 +231,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { bucketName: this.bucketName, objectKey, region: resolvedOr(this.stack.region, undefined), - assumeRoleArn: this.assetPublishingRoleArn, - assumeRoleExternalId: this.props.assetPublishingExternalId, + assumeRoleArn: this.fileAssetPublishingRoleArn, + assumeRoleExternalId: this.props.fileAssetPublishingExternalId, }, }, }; @@ -237,8 +269,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { repositoryName: this.repositoryName, imageTag, region: resolvedOr(this.stack.region, undefined), - assumeRoleArn: this.assetPublishingRoleArn, - assumeRoleExternalId: this.props.assetPublishingExternalId, + assumeRoleArn: this.imageAssetPublishingRoleArn, + assumeRoleExternalId: this.props.imageAssetPublishingExternalId, }, }, }; @@ -262,7 +294,7 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { assumeRoleArn: this._deployRoleArn, cloudFormationExecutionRoleArn: this._cloudFormationExecutionRoleArn, stackTemplateAssetObjectUrl: templateManifestUrl, - requiresBootstrapStackVersion: 1, + requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION, }, [artifactId]); } @@ -344,7 +376,7 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { type: cxschema.ArtifactType.ASSET_MANIFEST, properties: { file: manifestFile, - requiresBootstrapStackVersion: 1, + requiresBootstrapStackVersion: MIN_BOOTSTRAP_STACK_VERSION, }, }); diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index 7d5d41fe72feb..72980dfbfbfe2 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -839,24 +839,60 @@ export class Stack extends Construct implements ITaggable { } } -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; } /** diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index 485821ebe8fb7..a654253f2d938 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -151,7 +151,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/lodash": "^4.14.152", + "@types/lodash": "^4.14.155", "@types/node": "^10.17.21", "@types/nodeunit": "^0.0.31", "@types/minimatch": "^3.0.3", 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 index 723e7969c1d06..43591b9931148 100644 --- 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 @@ -2,7 +2,7 @@ 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, FileAssetPackaging, Stack } from '../../lib'; +import { App, CfnResource, DefaultStackSynthesizer, FileAssetPackaging, Stack } from '../../lib'; import { evaluateCFN } from '../evaluate-cfn'; const CFN_CONTEXT = { @@ -50,7 +50,7 @@ export = { 'current_account-current_region': { bucketName: 'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}', objectKey: '4bdae6e3b1b15f08c889d6c9133f24731ee14827a9a9ab9b6b6a9b42b6d34910', - assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-publishing-role-${AWS::AccountId}-${AWS::Region}', + assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}', }, }, }); @@ -106,22 +106,75 @@ export = { const asm = app.synth(); // THEN - we have an asset manifest with both assets and the stack template in there - 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 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, ...manifest.dockerImages})) { + 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.ok(destination.assumeRoleArn); + 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(); + }, }; /** @@ -135,4 +188,11 @@ function evalCFN(value: any) { 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.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/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 65f8c90f0bcb9..82fba93c7691a 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -73,13 +73,13 @@ "@aws-cdk/aws-ssm": "0.0.0", "@types/aws-lambda": "^8.10.39", "@types/fs-extra": "^8.1.0", - "@types/sinon": "^9.0.3", - "aws-sdk": "^2.681.0", + "@types/sinon": "^9.0.4", + "aws-sdk": "^2.689.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" diff --git a/packages/@aws-cdk/region-info/package.json b/packages/@aws-cdk/region-info/package.json index 4bfca87660b45..b15f9c8e7f3f7 100644 --- a/packages/@aws-cdk/region-info/package.json +++ b/packages/@aws-cdk/region-info/package.json @@ -55,7 +55,7 @@ "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" }, "repository": { 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/package.json b/packages/@monocdk-experiment/assert/package.json index ebd3b55c4681b..5da40f1a0097f 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -41,7 +41,7 @@ "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", - "ts-jest": "^26.0.0", + "ts-jest": "^26.1.0", "@monocdk-experiment/rewrite-imports": "0.0.0", "monocdk-experiment": "0.0.0", "constructs": "^3.0.2" diff --git a/packages/aws-cdk/.gitignore b/packages/aws-cdk/.gitignore index 35d7fd343f085..59c135c41f21b 100644 --- a/packages/aws-cdk/.gitignore +++ b/packages/aws-cdk/.gitignore @@ -32,5 +32,6 @@ cdk.context.json # as the subdirs contain .js files that should be committed) test/integ/cli/*.js test/integ/cli/*.d.ts -!test/integ/cli/jest.setup.js -!test/integ/cli/jest.config.js +!test/integ/cli-regression-patches/**/* + +.DS_Store diff --git a/packages/aws-cdk/.npmignore b/packages/aws-cdk/.npmignore index 2a37a179d8d4a..49b9729723982 100644 --- a/packages/aws-cdk/.npmignore +++ b/packages/aws-cdk/.npmignore @@ -25,3 +25,6 @@ tsconfig.json 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/lib/api/bootstrap/bootstrap-template.yaml b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml index 4da1a80bbeedc..5b61c2e99e7dd 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml @@ -106,7 +106,7 @@ Resources: Effect: Allow Principal: AWS: - Fn::Sub: "${PublishingRole.Arn}" + Fn::Sub: "${FilePublishingRole.Arn}" Resource: "*" Condition: CreateNewKey StagingBucket: @@ -158,7 +158,7 @@ Resources: - HasCustomContainerAssetsRepositoryName - Fn::Sub: "${ContainerAssetsRepositoryName}" - Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region} - PublishingRole: + FilePublishingRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: @@ -177,8 +177,28 @@ Resources: Ref: TrustedAccounts - Ref: AWS::NoValue RoleName: - Fn::Sub: cdk-${Qualifier}-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: @@ -206,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 @@ -223,9 +253,9 @@ Resources: Effect: Allow Version: '2012-10-17' Roles: - - Ref: PublishingRole + - Ref: ImagePublishingRole PolicyName: - Fn::Sub: cdk-${Qualifier}-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: @@ -317,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: CdkBootstrap-${Qualifier}-Version \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 99b18c3136b9f..583e6dc3d6ee8 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -235,6 +235,17 @@ export async function deployStack(options: DeployStackOptions): Promise=3.0", diff --git a/packages/aws-cdk/test/integ/cli-regression-patches/README.md b/packages/aws-cdk/test/integ/cli-regression-patches/README.md new file mode 100644 index 0000000000000..c930255e85809 --- /dev/null +++ b/packages/aws-cdk/test/integ/cli-regression-patches/README.md @@ -0,0 +1,54 @@ +Regression Test Patches +======================== + +The regression test suite will use the test suite of an OLD version +of the CLI when testing a NEW version of the CLI, to make sure the +old tests still pass. + +Sometimes though, the old tests won't pass. This can happen when we +introduce breaking changes to the framework or CLI (for something serious, +such as security reasons), or maybe because we had a bug in an old +version that happened to pass, but now the test needs to be updated +in order to pass a bugfix. + +## Mechanism + +The files in this directory will be copied over the test directory +so that you can exclude tests from running, or patch up test running +scripts. + +Files will be copied like so: + +``` +aws-cdk/test/integ/cli-regression-patches/vX.Y.Z/* + +# will be copied into + +aws-cdk/test/integ/cli +``` + +For example, to skip a certain integration test during regression +testing, create the following file: + +``` +cli-regression-patches/vX.Y.Z/skip-tests.txt +``` + +If you need to replace source files, it's probably best to stick +compiled `.js` files in here. `.ts` source files wouldn't compile +because they'd be missing `imports`. + +## Versioning + +The patch sets are versioned, so that they will only be applied for +a certain version of the tests and will automatically age out when +we proceed past that release. + +The version in the directory name needs to be named after the +version that contains the *tests* we're running, that need to be +patched. + +So for example, if we are running regression tests for release +candidate `1.45.0`, we would use the tests from released version +`1.44.0`, and so you would call the patch directory `v1.44.0`. + diff --git a/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/NOTES.md b/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/NOTES.md new file mode 100644 index 0000000000000..961813bc3107b --- /dev/null +++ b/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/NOTES.md @@ -0,0 +1,18 @@ +Patch notes: + +- Replace `test.sh` since we removed the old test exclusion + mechanism, and the `cli.exclusions.js` file that the old `test.sh` + depended upon. + +- We removed the old asset-publishing role from the new bootstrap + stack, and split it into separate file- and docker-publishing roles. + Since 1.44.0 would still expect the old asset-publishing role, + its test would fail, so we disable it: + +``` +test.skip('deploy new style synthesis to new style bootstrap', async () => { +``` + +There is a better mechanism for skipping certain tests by using `skip-tests.txt`, +but that one is only available AFTER this release, so for this version we just replace +source files. diff --git a/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/bootstrapping.integtest.js b/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/bootstrapping.integtest.js new file mode 100644 index 0000000000000..d715afa923e1d --- /dev/null +++ b/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/bootstrapping.integtest.js @@ -0,0 +1,126 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const aws_helpers_1 = require("./aws-helpers"); +const cdk_helpers_1 = require("./cdk-helpers"); +jest.setTimeout(600000); +const QUALIFIER = randomString(); +beforeAll(async () => { + await cdk_helpers_1.prepareAppFixture(); +}); +beforeEach(async () => { + await cdk_helpers_1.cleanup(); +}); +afterEach(async () => { + await cdk_helpers_1.cleanup(); +}); +test('can bootstrap without execution', async () => { + var _a; + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--no-execute']); + const resp = await aws_helpers_1.cloudFormation('describeStacks', { + StackName: bootstrapStackName, + }); + expect((_a = resp.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); +}); +test('upgrade legacy bootstrap stack to new bootstrap stack while in use', async () => { + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + const legacyBootstrapBucketName = `aws-cdk-bootstrap-integ-test-legacy-bckt-${randomString()}`; + const newBootstrapBucketName = `aws-cdk-bootstrap-integ-test-v2-bckt-${randomString()}`; + cdk_helpers_1.rememberToDeleteBucket(legacyBootstrapBucketName); // This one will leak + cdk_helpers_1.rememberToDeleteBucket(newBootstrapBucketName); // This one shouldn't leak if the test succeeds, but let's be safe in case it doesn't + // Legacy bootstrap + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--bootstrap-bucket-name', legacyBootstrapBucketName]); + // Deploy stack that uses file assets + await cdk_helpers_1.cdkDeploy('lambda', { + options: ['--toolkit-stack-name', bootstrapStackName], + }); + // Upgrade bootstrap stack to "new" style + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--bootstrap-bucket-name', newBootstrapBucketName, + '--qualifier', QUALIFIER], { + modEnv: { + CDK_NEW_BOOTSTRAP: '1', + }, + }); + // (Force) deploy stack again + // --force to bypass the check which says that the template hasn't changed. + await cdk_helpers_1.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--force', + ], + }); +}); +test.skip('deploy new style synthesis to new style bootstrap', async () => { + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--qualifier', QUALIFIER, + '--cloudformation-execution-policies', 'arn:aws:iam::aws:policy/AdministratorAccess', + ], { + modEnv: { + CDK_NEW_BOOTSTRAP: '1', + }, + }); + // Deploy stack that uses file assets + await cdk_helpers_1.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${QUALIFIER}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + }); +}); +test('deploy old style synthesis to new style bootstrap', async () => { + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--qualifier', QUALIFIER, + '--cloudformation-execution-policies', 'arn:aws:iam::aws:policy/AdministratorAccess', + ], { + modEnv: { + CDK_NEW_BOOTSTRAP: '1', + }, + }); + // Deploy stack that uses file assets + await cdk_helpers_1.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + ], + }); +}); +test('deploying new style synthesis to old style bootstrap fails', async () => { + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + await cdk_helpers_1.cdk(['bootstrap', '--toolkit-stack-name', bootstrapStackName]); + // Deploy stack that uses file assets, this fails because the bootstrap stack + // is version checked. + await expect(cdk_helpers_1.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + })).rejects.toThrow('exited with error'); +}); +test('can create multiple legacy bootstrap stacks', async () => { + var _a; + const bootstrapStackName1 = cdk_helpers_1.fullStackName('bootstrap-stack-1'); + const bootstrapStackName2 = cdk_helpers_1.fullStackName('bootstrap-stack-2'); + // deploy two toolkit stacks into the same environment (see #1416) + // one with tags + await cdk_helpers_1.cdk(['bootstrap', '-v', '--toolkit-stack-name', bootstrapStackName1, '--tags', 'Foo=Bar']); + await cdk_helpers_1.cdk(['bootstrap', '-v', '--toolkit-stack-name', bootstrapStackName2]); + const response = await aws_helpers_1.cloudFormation('describeStacks', { StackName: bootstrapStackName1 }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Tags).toEqual([ + { Key: 'Foo', Value: 'Bar' }, + ]); +}); +function randomString() { + // Crazy + return Math.random().toString(36).replace(/[^a-z0-9]+/g, ''); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYm9vdHN0cmFwcGluZy5pbnRlZ3Rlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJib290c3RyYXBwaW5nLmludGVndGVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLCtDQUErQztBQUMvQywrQ0FBa0g7QUFFbEgsSUFBSSxDQUFDLFVBQVUsQ0FBQyxNQUFPLENBQUMsQ0FBQztBQUV6QixNQUFNLFNBQVMsR0FBRyxZQUFZLEVBQUUsQ0FBQztBQUVqQyxTQUFTLENBQUMsS0FBSyxJQUFJLEVBQUU7SUFDbkIsTUFBTSwrQkFBaUIsRUFBRSxDQUFDO0FBQzVCLENBQUMsQ0FBQyxDQUFDO0FBRUgsVUFBVSxDQUFDLEtBQUssSUFBSSxFQUFFO0lBQ3BCLE1BQU0scUJBQU8sRUFBRSxDQUFDO0FBQ2xCLENBQUMsQ0FBQyxDQUFDO0FBRUgsU0FBUyxDQUFDLEtBQUssSUFBSSxFQUFFO0lBQ25CLE1BQU0scUJBQU8sRUFBRSxDQUFDO0FBQ2xCLENBQUMsQ0FBQyxDQUFDO0FBRUgsSUFBSSxDQUFDLGlDQUFpQyxFQUFFLEtBQUssSUFBSSxFQUFFOztJQUNqRCxNQUFNLGtCQUFrQixHQUFHLDJCQUFhLENBQUMsaUJBQWlCLENBQUMsQ0FBQztJQUU1RCxNQUFNLGlCQUFHLENBQUMsQ0FBQyxXQUFXO1FBQ3BCLHNCQUFzQixFQUFFLGtCQUFrQjtRQUMxQyxjQUFjLENBQUMsQ0FBQyxDQUFDO0lBRW5CLE1BQU0sSUFBSSxHQUFHLE1BQU0sNEJBQWMsQ0FBQyxnQkFBZ0IsRUFBRTtRQUNsRCxTQUFTLEVBQUUsa0JBQWtCO0tBQzlCLENBQUMsQ0FBQztJQUVILE1BQU0sT0FBQyxJQUFJLENBQUMsTUFBTSwwQ0FBRyxDQUFDLEVBQUUsV0FBVyxDQUFDLENBQUMsT0FBTyxDQUFDLG9CQUFvQixDQUFDLENBQUM7QUFDckUsQ0FBQyxDQUFDLENBQUM7QUFFSCxJQUFJLENBQUMsb0VBQW9FLEVBQUUsS0FBSyxJQUFJLEVBQUU7SUFDcEYsTUFBTSxrQkFBa0IsR0FBRywyQkFBYSxDQUFDLGlCQUFpQixDQUFDLENBQUM7SUFFNUQsTUFBTSx5QkFBeUIsR0FBRyw0Q0FBNEMsWUFBWSxFQUFFLEVBQUUsQ0FBQztJQUMvRixNQUFNLHNCQUFzQixHQUFHLHdDQUF3QyxZQUFZLEVBQUUsRUFBRSxDQUFDO0lBQ3hGLG9DQUFzQixDQUFDLHlCQUF5QixDQUFDLENBQUMsQ0FBRSxxQkFBcUI7SUFDekUsb0NBQXNCLENBQUMsc0JBQXNCLENBQUMsQ0FBQyxDQUFLLHFGQUFxRjtJQUV6SSxtQkFBbUI7SUFDbkIsTUFBTSxpQkFBRyxDQUFDLENBQUMsV0FBVztRQUNwQixzQkFBc0IsRUFBRSxrQkFBa0I7UUFDMUMseUJBQXlCLEVBQUUseUJBQXlCLENBQUMsQ0FBQyxDQUFDO0lBRXpELHFDQUFxQztJQUNyQyxNQUFNLHVCQUFTLENBQUMsUUFBUSxFQUFFO1FBQ3hCLE9BQU8sRUFBRSxDQUFDLHNCQUFzQixFQUFFLGtCQUFrQixDQUFDO0tBQ3RELENBQUMsQ0FBQztJQUVILHlDQUF5QztJQUN6QyxNQUFNLGlCQUFHLENBQUMsQ0FBQyxXQUFXO1FBQ3BCLHNCQUFzQixFQUFFLGtCQUFrQjtRQUMxQyx5QkFBeUIsRUFBRSxzQkFBc0I7UUFDakQsYUFBYSxFQUFFLFNBQVMsQ0FBQyxFQUFFO1FBQzNCLE1BQU0sRUFBRTtZQUNOLGlCQUFpQixFQUFFLEdBQUc7U0FDdkI7S0FDRixDQUFDLENBQUM7SUFFSCw2QkFBNkI7SUFDN0IsMkVBQTJFO0lBQzNFLE1BQU0sdUJBQVMsQ0FBQyxRQUFRLEVBQUU7UUFDeEIsT0FBTyxFQUFFO1lBQ1Asc0JBQXNCLEVBQUUsa0JBQWtCO1lBQzFDLFNBQVM7U0FDVjtLQUNGLENBQUMsQ0FBQztBQUNMLENBQUMsQ0FBQyxDQUFDO0FBRUgsSUFBSSxDQUFDLG1EQUFtRCxFQUFFLEtBQUssSUFBSSxFQUFFO0lBQ25FLE1BQU0sa0JBQWtCLEdBQUcsMkJBQWEsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBRTVELE1BQU0saUJBQUcsQ0FBQyxDQUFDLFdBQVc7UUFDcEIsc0JBQXNCLEVBQUUsa0JBQWtCO1FBQzFDLGFBQWEsRUFBRSxTQUFTO1FBQ3hCLHFDQUFxQyxFQUFFLDZDQUE2QztLQUNyRixFQUFFO1FBQ0QsTUFBTSxFQUFFO1lBQ04saUJBQWlCLEVBQUUsR0FBRztTQUN2QjtLQUNGLENBQUMsQ0FBQztJQUVILHFDQUFxQztJQUNyQyxNQUFNLHVCQUFTLENBQUMsUUFBUSxFQUFFO1FBQ3hCLE9BQU8sRUFBRTtZQUNQLHNCQUFzQixFQUFFLGtCQUFrQjtZQUMxQyxXQUFXLEVBQUUsb0NBQW9DLFNBQVMsRUFBRTtZQUM1RCxXQUFXLEVBQUUsd0NBQXdDO1NBQ3REO0tBQ0YsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDLENBQUM7QUFFSCxJQUFJLENBQUMsbURBQW1ELEVBQUUsS0FBSyxJQUFJLEVBQUU7SUFDbkUsTUFBTSxrQkFBa0IsR0FBRywyQkFBYSxDQUFDLGlCQUFpQixDQUFDLENBQUM7SUFFNUQsTUFBTSxpQkFBRyxDQUFDLENBQUMsV0FBVztRQUNwQixzQkFBc0IsRUFBRSxrQkFBa0I7UUFDMUMsYUFBYSxFQUFFLFNBQVM7UUFDeEIscUNBQXFDLEVBQUUsNkNBQTZDO0tBQ3JGLEVBQUU7UUFDRCxNQUFNLEVBQUU7WUFDTixpQkFBaUIsRUFBRSxHQUFHO1NBQ3ZCO0tBQ0YsQ0FBQyxDQUFDO0lBRUgscUNBQXFDO0lBQ3JDLE1BQU0sdUJBQVMsQ0FBQyxRQUFRLEVBQUU7UUFDeEIsT0FBTyxFQUFFO1lBQ1Asc0JBQXNCLEVBQUUsa0JBQWtCO1NBQzNDO0tBQ0YsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDLENBQUM7QUFFSCxJQUFJLENBQUMsNERBQTRELEVBQUUsS0FBSyxJQUFJLEVBQUU7SUFDNUUsTUFBTSxrQkFBa0IsR0FBRywyQkFBYSxDQUFDLGlCQUFpQixDQUFDLENBQUM7SUFFNUQsTUFBTSxpQkFBRyxDQUFDLENBQUMsV0FBVyxFQUFFLHNCQUFzQixFQUFFLGtCQUFrQixDQUFDLENBQUMsQ0FBQztJQUVyRSw2RUFBNkU7SUFDN0Usc0JBQXNCO0lBQ3RCLE1BQU0sTUFBTSxDQUFDLHVCQUFTLENBQUMsUUFBUSxFQUFFO1FBQy9CLE9BQU8sRUFBRTtZQUNQLHNCQUFzQixFQUFFLGtCQUFrQjtZQUMxQyxXQUFXLEVBQUUsd0NBQXdDO1NBQ3REO0tBQ0YsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0FBQzNDLENBQUMsQ0FBQyxDQUFDO0FBRUgsSUFBSSxDQUFDLDZDQUE2QyxFQUFFLEtBQUssSUFBSSxFQUFFOztJQUM3RCxNQUFNLG1CQUFtQixHQUFHLDJCQUFhLENBQUMsbUJBQW1CLENBQUMsQ0FBQztJQUMvRCxNQUFNLG1CQUFtQixHQUFHLDJCQUFhLENBQUMsbUJBQW1CLENBQUMsQ0FBQztJQUUvRCxrRUFBa0U7SUFDbEUsZ0JBQWdCO0lBQ2hCLE1BQU0saUJBQUcsQ0FBQyxDQUFDLFdBQVcsRUFBRSxJQUFJLEVBQUUsc0JBQXNCLEVBQUUsbUJBQW1CLEVBQUUsUUFBUSxFQUFFLFNBQVMsQ0FBQyxDQUFDLENBQUM7SUFDakcsTUFBTSxpQkFBRyxDQUFDLENBQUMsV0FBVyxFQUFFLElBQUksRUFBRSxzQkFBc0IsRUFBRSxtQkFBbUIsQ0FBQyxDQUFDLENBQUM7SUFFNUUsTUFBTSxRQUFRLEdBQUcsTUFBTSw0QkFBYyxDQUFDLGdCQUFnQixFQUFFLEVBQUUsU0FBUyxFQUFFLG1CQUFtQixFQUFFLENBQUMsQ0FBQztJQUM1RixNQUFNLE9BQUMsUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLElBQUksQ0FBQyxDQUFDLE9BQU8sQ0FBQztRQUN4QyxFQUFFLEdBQUcsRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRTtLQUM3QixDQUFDLENBQUM7QUFDTCxDQUFDLENBQUMsQ0FBQztBQUVILFNBQVMsWUFBWTtJQUNuQixRQUFRO0lBQ1IsT0FBTyxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxhQUFhLEVBQUUsRUFBRSxDQUFDLENBQUM7QUFDL0QsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNsb3VkRm9ybWF0aW9uIH0gZnJvbSAnLi9hd3MtaGVscGVycyc7XG5pbXBvcnQgeyBjZGssIGNka0RlcGxveSwgY2xlYW51cCwgZnVsbFN0YWNrTmFtZSwgcHJlcGFyZUFwcEZpeHR1cmUsIHJlbWVtYmVyVG9EZWxldGVCdWNrZXQgfSBmcm9tICcuL2Nkay1oZWxwZXJzJztcblxuamVzdC5zZXRUaW1lb3V0KDYwMF8wMDApO1xuXG5jb25zdCBRVUFMSUZJRVIgPSByYW5kb21TdHJpbmcoKTtcblxuYmVmb3JlQWxsKGFzeW5jICgpID0+IHtcbiAgYXdhaXQgcHJlcGFyZUFwcEZpeHR1cmUoKTtcbn0pO1xuXG5iZWZvcmVFYWNoKGFzeW5jICgpID0+IHtcbiAgYXdhaXQgY2xlYW51cCgpO1xufSk7XG5cbmFmdGVyRWFjaChhc3luYyAoKSA9PiB7XG4gIGF3YWl0IGNsZWFudXAoKTtcbn0pO1xuXG50ZXN0KCdjYW4gYm9vdHN0cmFwIHdpdGhvdXQgZXhlY3V0aW9uJywgYXN5bmMgKCkgPT4ge1xuICBjb25zdCBib290c3RyYXBTdGFja05hbWUgPSBmdWxsU3RhY2tOYW1lKCdib290c3RyYXAtc3RhY2snKTtcblxuICBhd2FpdCBjZGsoWydib290c3RyYXAnLFxuICAgICctLXRvb2xraXQtc3RhY2stbmFtZScsIGJvb3RzdHJhcFN0YWNrTmFtZSxcbiAgICAnLS1uby1leGVjdXRlJ10pO1xuXG4gIGNvbnN0IHJlc3AgPSBhd2FpdCBjbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBib290c3RyYXBTdGFja05hbWUsXG4gIH0pO1xuXG4gIGV4cGVjdChyZXNwLlN0YWNrcz8uWzBdLlN0YWNrU3RhdHVzKS50b0VxdWFsKCdSRVZJRVdfSU5fUFJPR1JFU1MnKTtcbn0pO1xuXG50ZXN0KCd1cGdyYWRlIGxlZ2FjeSBib290c3RyYXAgc3RhY2sgdG8gbmV3IGJvb3RzdHJhcCBzdGFjayB3aGlsZSBpbiB1c2UnLCBhc3luYyAoKSA9PiB7XG4gIGNvbnN0IGJvb3RzdHJhcFN0YWNrTmFtZSA9IGZ1bGxTdGFja05hbWUoJ2Jvb3RzdHJhcC1zdGFjaycpO1xuXG4gIGNvbnN0IGxlZ2FjeUJvb3RzdHJhcEJ1Y2tldE5hbWUgPSBgYXdzLWNkay1ib290c3RyYXAtaW50ZWctdGVzdC1sZWdhY3ktYmNrdC0ke3JhbmRvbVN0cmluZygpfWA7XG4gIGNvbnN0IG5ld0Jvb3RzdHJhcEJ1Y2tldE5hbWUgPSBgYXdzLWNkay1ib290c3RyYXAtaW50ZWctdGVzdC12Mi1iY2t0LSR7cmFuZG9tU3RyaW5nKCl9YDtcbiAgcmVtZW1iZXJUb0RlbGV0ZUJ1Y2tldChsZWdhY3lCb290c3RyYXBCdWNrZXROYW1lKTsgIC8vIFRoaXMgb25lIHdpbGwgbGVha1xuICByZW1lbWJlclRvRGVsZXRlQnVja2V0KG5ld0Jvb3RzdHJhcEJ1Y2tldE5hbWUpOyAgICAgLy8gVGhpcyBvbmUgc2hvdWxkbid0IGxlYWsgaWYgdGhlIHRlc3Qgc3VjY2VlZHMsIGJ1dCBsZXQncyBiZSBzYWZlIGluIGNhc2UgaXQgZG9lc24ndFxuXG4gIC8vIExlZ2FjeSBib290c3RyYXBcbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJyxcbiAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgJy0tYm9vdHN0cmFwLWJ1Y2tldC1uYW1lJywgbGVnYWN5Qm9vdHN0cmFwQnVja2V0TmFtZV0pO1xuXG4gIC8vIERlcGxveSBzdGFjayB0aGF0IHVzZXMgZmlsZSBhc3NldHNcbiAgYXdhaXQgY2RrRGVwbG95KCdsYW1iZGEnLCB7XG4gICAgb3B0aW9uczogWyctLXRvb2xraXQtc3RhY2stbmFtZScsIGJvb3RzdHJhcFN0YWNrTmFtZV0sXG4gIH0pO1xuXG4gIC8vIFVwZ3JhZGUgYm9vdHN0cmFwIHN0YWNrIHRvIFwibmV3XCIgc3R5bGVcbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJyxcbiAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgJy0tYm9vdHN0cmFwLWJ1Y2tldC1uYW1lJywgbmV3Qm9vdHN0cmFwQnVja2V0TmFtZSxcbiAgICAnLS1xdWFsaWZpZXInLCBRVUFMSUZJRVJdLCB7XG4gICAgbW9kRW52OiB7XG4gICAgICBDREtfTkVXX0JPT1RTVFJBUDogJzEnLFxuICAgIH0sXG4gIH0pO1xuXG4gIC8vIChGb3JjZSkgZGVwbG95IHN0YWNrIGFnYWluXG4gIC8vIC0tZm9yY2UgdG8gYnlwYXNzIHRoZSBjaGVjayB3aGljaCBzYXlzIHRoYXQgdGhlIHRlbXBsYXRlIGhhc24ndCBjaGFuZ2VkLlxuICBhd2FpdCBjZGtEZXBsb3koJ2xhbWJkYScsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgICAnLS1mb3JjZScsXG4gICAgXSxcbiAgfSk7XG59KTtcblxudGVzdCgnZGVwbG95IG5ldyBzdHlsZSBzeW50aGVzaXMgdG8gbmV3IHN0eWxlIGJvb3RzdHJhcCcsIGFzeW5jICgpID0+IHtcbiAgY29uc3QgYm9vdHN0cmFwU3RhY2tOYW1lID0gZnVsbFN0YWNrTmFtZSgnYm9vdHN0cmFwLXN0YWNrJyk7XG5cbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJyxcbiAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgJy0tcXVhbGlmaWVyJywgUVVBTElGSUVSLFxuICAgICctLWNsb3VkZm9ybWF0aW9uLWV4ZWN1dGlvbi1wb2xpY2llcycsICdhcm46YXdzOmlhbTo6YXdzOnBvbGljeS9BZG1pbmlzdHJhdG9yQWNjZXNzJyxcbiAgXSwge1xuICAgIG1vZEVudjoge1xuICAgICAgQ0RLX05FV19CT09UU1RSQVA6ICcxJyxcbiAgICB9LFxuICB9KTtcblxuICAvLyBEZXBsb3kgc3RhY2sgdGhhdCB1c2VzIGZpbGUgYXNzZXRzXG4gIGF3YWl0IGNka0RlcGxveSgnbGFtYmRhJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXRvb2xraXQtc3RhY2stbmFtZScsIGJvb3RzdHJhcFN0YWNrTmFtZSxcbiAgICAgICctLWNvbnRleHQnLCBgQGF3cy1jZGsvY29yZTpib290c3RyYXBRdWFsaWZpZXI9JHtRVUFMSUZJRVJ9YCxcbiAgICAgICctLWNvbnRleHQnLCAnQGF3cy1jZGsvY29yZTpuZXdTdHlsZVN0YWNrU3ludGhlc2lzPTEnLFxuICAgIF0sXG4gIH0pO1xufSk7XG5cbnRlc3QoJ2RlcGxveSBvbGQgc3R5bGUgc3ludGhlc2lzIHRvIG5ldyBzdHlsZSBib290c3RyYXAnLCBhc3luYyAoKSA9PiB7XG4gIGNvbnN0IGJvb3RzdHJhcFN0YWNrTmFtZSA9IGZ1bGxTdGFja05hbWUoJ2Jvb3RzdHJhcC1zdGFjaycpO1xuXG4gIGF3YWl0IGNkayhbJ2Jvb3RzdHJhcCcsXG4gICAgJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgICctLXF1YWxpZmllcicsIFFVQUxJRklFUixcbiAgICAnLS1jbG91ZGZvcm1hdGlvbi1leGVjdXRpb24tcG9saWNpZXMnLCAnYXJuOmF3czppYW06OmF3czpwb2xpY3kvQWRtaW5pc3RyYXRvckFjY2VzcycsXG4gIF0sIHtcbiAgICBtb2RFbnY6IHtcbiAgICAgIENES19ORVdfQk9PVFNUUkFQOiAnMScsXG4gICAgfSxcbiAgfSk7XG5cbiAgLy8gRGVwbG95IHN0YWNrIHRoYXQgdXNlcyBmaWxlIGFzc2V0c1xuICBhd2FpdCBjZGtEZXBsb3koJ2xhbWJkYScsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgXSxcbiAgfSk7XG59KTtcblxudGVzdCgnZGVwbG95aW5nIG5ldyBzdHlsZSBzeW50aGVzaXMgdG8gb2xkIHN0eWxlIGJvb3RzdHJhcCBmYWlscycsIGFzeW5jICgpID0+IHtcbiAgY29uc3QgYm9vdHN0cmFwU3RhY2tOYW1lID0gZnVsbFN0YWNrTmFtZSgnYm9vdHN0cmFwLXN0YWNrJyk7XG5cbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJywgJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lXSk7XG5cbiAgLy8gRGVwbG95IHN0YWNrIHRoYXQgdXNlcyBmaWxlIGFzc2V0cywgdGhpcyBmYWlscyBiZWNhdXNlIHRoZSBib290c3RyYXAgc3RhY2tcbiAgLy8gaXMgdmVyc2lvbiBjaGVja2VkLlxuICBhd2FpdCBleHBlY3QoY2RrRGVwbG95KCdsYW1iZGEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgICAgJy0tY29udGV4dCcsICdAYXdzLWNkay9jb3JlOm5ld1N0eWxlU3RhY2tTeW50aGVzaXM9MScsXG4gICAgXSxcbiAgfSkpLnJlamVjdHMudG9UaHJvdygnZXhpdGVkIHdpdGggZXJyb3InKTtcbn0pO1xuXG50ZXN0KCdjYW4gY3JlYXRlIG11bHRpcGxlIGxlZ2FjeSBib290c3RyYXAgc3RhY2tzJywgYXN5bmMgKCkgPT4ge1xuICBjb25zdCBib290c3RyYXBTdGFja05hbWUxID0gZnVsbFN0YWNrTmFtZSgnYm9vdHN0cmFwLXN0YWNrLTEnKTtcbiAgY29uc3QgYm9vdHN0cmFwU3RhY2tOYW1lMiA9IGZ1bGxTdGFja05hbWUoJ2Jvb3RzdHJhcC1zdGFjay0yJyk7XG5cbiAgLy8gZGVwbG95IHR3byB0b29sa2l0IHN0YWNrcyBpbnRvIHRoZSBzYW1lIGVudmlyb25tZW50IChzZWUgIzE0MTYpXG4gIC8vIG9uZSB3aXRoIHRhZ3NcbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJywgJy12JywgJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lMSwgJy0tdGFncycsICdGb289QmFyJ10pO1xuICBhd2FpdCBjZGsoWydib290c3RyYXAnLCAnLXYnLCAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUyXSk7XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBjbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7IFN0YWNrTmFtZTogYm9vdHN0cmFwU3RhY2tOYW1lMSB9KTtcbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlRhZ3MpLnRvRXF1YWwoW1xuICAgIHsgS2V5OiAnRm9vJywgVmFsdWU6ICdCYXInIH0sXG4gIF0pO1xufSk7XG5cbmZ1bmN0aW9uIHJhbmRvbVN0cmluZygpIHtcbiAgLy8gQ3JhenlcbiAgcmV0dXJuIE1hdGgucmFuZG9tKCkudG9TdHJpbmcoMzYpLnJlcGxhY2UoL1teYS16MC05XSsvZywgJycpO1xufVxuIl19 diff --git a/packages/aws-cdk/test/integ/cli/test-jest.sh b/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/test.sh similarity index 52% rename from packages/aws-cdk/test/integ/cli/test-jest.sh rename to packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/test.sh index 3367ac0129919..482956df450f4 100755 --- a/packages/aws-cdk/test/integ/cli/test-jest.sh +++ b/packages/aws-cdk/test/integ/cli-regression-patches/v1.44.0/test.sh @@ -1,10 +1,17 @@ #!/bin/bash -# A number of tests have been written in TS/Jest, instead of bash. -# This script runs them. - set -euo pipefail scriptdir=$(cd $(dirname $0) && pwd) +echo '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' +echo 'CLI Integration Tests' +echo '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' + +current_version=$(node -p "require('${scriptdir}/../../../package.json').version") + +# This allows injecting different versions, not just the current one. +# Useful when testing. +export VERSION_UNDER_TEST=${VERSION_UNDER_TEST:-${current_version}} + cd $scriptdir # Install these dependencies that the tests (written in Jest) need. @@ -16,4 +23,4 @@ if ! npx --no-install jest --version; then npm install --prefix . jest aws-sdk fi -npx jest --runInBand --verbose --setupFilesAfterEnv "$PWD/jest.setup.js" "$@" +npx jest --runInBand --verbose "$@" \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli.exclusions.js b/packages/aws-cdk/test/integ/cli.exclusions.js deleted file mode 100644 index 4204b62bd527c..0000000000000 --- a/packages/aws-cdk/test/integ/cli.exclusions.js +++ /dev/null @@ -1,70 +0,0 @@ -/** -List of exclusions when running backwards compatibility tests. -Add when you need to exclude a specific integration test from a specific version. - -This is an escape hatch for the rare cases where we need to introduce -a change that breaks existing integration tests. (e.g security) - -For example: - -{ - "test": "test-cdk-iam-diff.sh", - "version": "v1.30.0", - "justification": "iam policy generation has changed in version > 1.30.0 because..." -}, - -*/ -const exclusions = [ - { - "test": "test-cdk-deploy-nested-stack-with-parameters.sh", - "version": "v1.37.0", - "justification": "This test doesn't use a unique sns topic name for the topic in the nested stack and it collides with our regular integ suite" - }, - { - "test": "test-cdk-deploy-wildcard-with-outputs.sh", - "version": "v1.37.0", - "justification": "This test doesn't use a unique sns topic name and it collides with our regular integ suite" - }, - { - "test": "test-cdk-deploy-with-outputs.sh", - "version": "v1.37.0", - "justification": "This test doesn't use a unique sns topic name and it collides with our regular integ suite" - } -] - -function getExclusion(test, version) { - - const filtered = exclusions.filter(e => { - return e.test === test && e.version === version; - }); - - if (filtered.length === 0) { - return undefined; - } - - if (filtered.length === 1) { - return filtered[0]; - } - - throw new Error(`Multiple exclusions found for (${test, version}): ${filtered.length}`); - -} - -module.exports.shouldSkip = function (test, version) { - - const exclusion = getExclusion(test, version); - - return exclusion != undefined - -} - -module.exports.getJustification = function (test, version) { - - const exclusion = getExclusion(test, version); - - if (!exclusion) { - throw new Error(`Exclusion not found for (${test}, ${version})`); - } - - return exclusion.justification; -} diff --git a/packages/aws-cdk/test/integ/cli/README.md b/packages/aws-cdk/test/integ/cli/README.md index 44d531623e112..9e0e8d9b5e5f1 100644 --- a/packages/aws-cdk/test/integ/cli/README.md +++ b/packages/aws-cdk/test/integ/cli/README.md @@ -20,9 +20,6 @@ Running against a failing dist build: ## Adding tests -Older tests were written in bash; new tests should be written in -TypeScript/Jest, that is much more comfortable to write in. - Even though tests are now written in TypeScript, this does not conceptually change their SUT! They are still testing the CLI via running it as a subprocess, they are NOT reaching directly into the CLI @@ -34,8 +31,8 @@ Compilation of the tests is done as part of the normal package build, at which point it is using the dependencies brought in by the containing `aws-cdk` package's `package.json`. -When run in a non-develompent repo (as done during integ tests or canary runs), -the required dependencies are brought in just-in-time via `test-jest.sh`. Any +When run in a non-development repo (as done during integ tests or canary runs), +the required dependencies are brought in just-in-time via `test.sh`. Any new dependencies added for the tests should be added there as well. But, better yet, don't add any dependencies at all. You shouldn't need to, these tests are simple. diff --git a/packages/aws-cdk/test/integ/cli/app/app.js b/packages/aws-cdk/test/integ/cli/app/app.js index 33e2802ad2ac7..a61bc4b798b32 100644 --- a/packages/aws-cdk/test/integ/cli/app/app.js +++ b/packages/aws-cdk/test/integ/cli/app/app.js @@ -107,7 +107,7 @@ class IamStack extends cdk.Stack { super(parent, id, props); new iam.Role(this, 'SomeRole', { - assumedBy: new iam.ServicePrincipal('ec2.amazon.aws.com') + assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') }); } } @@ -255,7 +255,7 @@ new MultiParameterStack(app, `${stackPrefix}-param-test-3`); new OutputsStack(app, `${stackPrefix}-outputs-test-1`); new AnotherOutputsStack(app, `${stackPrefix}-outputs-test-2`); // Not included in wildcard -new IamStack(app, `${stackPrefix}-iam-test`); +new IamStack(app, `${stackPrefix}-iam-test`, { env: defaultEnv }); const providing = new ProvidingStack(app, `${stackPrefix}-order-providing`); new ConsumingStack(app, `${stackPrefix}-order-consuming`, { providingStack: providing }); @@ -280,7 +280,7 @@ new StackWithNestedStack(app, `${stackPrefix}-with-nested-stack`); new StackWithNestedStackUsingParameters(app, `${stackPrefix}-with-nested-stack-using-parameters`); new YourStack(app, `${stackPrefix}-termination-protection`, { - terminationProtection: true, + terminationProtection: process.env.TERMINATION_PROTECTION !== 'FALSE' ? true : false, }); app.synth(); diff --git a/packages/aws-cdk/test/integ/cli/aws-helpers.ts b/packages/aws-cdk/test/integ/cli/aws-helpers.ts index 92cb7a77131a3..fb54db4f60bcd 100644 --- a/packages/aws-cdk/test/integ/cli/aws-helpers.ts +++ b/packages/aws-cdk/test/integ/cli/aws-helpers.ts @@ -20,6 +20,7 @@ export let testEnv = async (): Promise => { export const cloudFormation = makeAwsCaller(AWS.CloudFormation); export const s3 = makeAwsCaller(AWS.S3); +export const ecr = makeAwsCaller(AWS.ECR); export const sns = makeAwsCaller(AWS.SNS); export const iam = makeAwsCaller(AWS.IAM); export const lambda = makeAwsCaller(AWS.Lambda); @@ -188,6 +189,10 @@ export async function emptyBucket(bucketName: string) { }); } +export async function deleteImageRepository(repositoryName: string) { + await ecr('deleteRepository', { repositoryName, force: true }); +} + export async function deleteBucket(bucketName: string) { try { await emptyBucket(bucketName); diff --git a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts index 3c674470abe4c..93f9a0974aa2a 100644 --- a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts @@ -1,5 +1,6 @@ import { cloudFormation } from './aws-helpers'; import { cdk, cdkDeploy, cleanup, fullStackName, prepareAppFixture, rememberToDeleteBucket } from './cdk-helpers'; +import { integTest } from './test-helpers'; jest.setTimeout(600_000); @@ -17,7 +18,7 @@ afterEach(async () => { await cleanup(); }); -test('can bootstrap without execution', async () => { +integTest('can bootstrap without execution', async () => { const bootstrapStackName = fullStackName('bootstrap-stack'); await cdk(['bootstrap', @@ -31,7 +32,7 @@ test('can bootstrap without execution', async () => { expect(resp.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); }); -test('upgrade legacy bootstrap stack to new bootstrap stack while in use', async () => { +integTest('upgrade legacy bootstrap stack to new bootstrap stack while in use', async () => { const bootstrapStackName = fullStackName('bootstrap-stack'); const legacyBootstrapBucketName = `aws-cdk-bootstrap-integ-test-legacy-bckt-${randomString()}`; @@ -69,7 +70,7 @@ test('upgrade legacy bootstrap stack to new bootstrap stack while in use', async }); }); -test('deploy new style synthesis to new style bootstrap', async () => { +integTest('deploy new style synthesis to new style bootstrap', async () => { const bootstrapStackName = fullStackName('bootstrap-stack'); await cdk(['bootstrap', @@ -92,7 +93,30 @@ test('deploy new style synthesis to new style bootstrap', async () => { }); }); -test('deploy old style synthesis to new style bootstrap', async () => { +integTest('deploy new style synthesis to new style bootstrap (with docker image)', async () => { + const bootstrapStackName = fullStackName('bootstrap-stack'); + + await cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--qualifier', QUALIFIER, + '--cloudformation-execution-policies', 'arn:aws:iam::aws:policy/AdministratorAccess', + ], { + modEnv: { + CDK_NEW_BOOTSTRAP: '1', + }, + }); + + // Deploy stack that uses file assets + await cdkDeploy('docker', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${QUALIFIER}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + }); +}); + +integTest('deploy old style synthesis to new style bootstrap', async () => { const bootstrapStackName = fullStackName('bootstrap-stack'); await cdk(['bootstrap', @@ -113,7 +137,7 @@ test('deploy old style synthesis to new style bootstrap', async () => { }); }); -test('deploying new style synthesis to old style bootstrap fails', async () => { +integTest('deploying new style synthesis to old style bootstrap fails', async () => { const bootstrapStackName = fullStackName('bootstrap-stack'); await cdk(['bootstrap', '--toolkit-stack-name', bootstrapStackName]); @@ -128,7 +152,7 @@ test('deploying new style synthesis to old style bootstrap fails', async () => { })).rejects.toThrow('exited with error'); }); -test('can create multiple legacy bootstrap stacks', async () => { +integTest('can create multiple legacy bootstrap stacks', async () => { const bootstrapStackName1 = fullStackName('bootstrap-stack-1'); const bootstrapStackName2 = fullStackName('bootstrap-stack-2'); diff --git a/packages/aws-cdk/test/integ/cli/cdk-helpers.ts b/packages/aws-cdk/test/integ/cli/cdk-helpers.ts index 9256a56d7dc00..410b8d71d9e71 100644 --- a/packages/aws-cdk/test/integ/cli/cdk-helpers.ts +++ b/packages/aws-cdk/test/integ/cli/cdk-helpers.ts @@ -1,7 +1,7 @@ import * as child_process from 'child_process'; import * as os from 'os'; import * as path from 'path'; -import { cloudFormation, deleteBucket, deleteStacks, emptyBucket, outputFromStack, testEnv } from './aws-helpers'; +import { cloudFormation, deleteBucket, deleteImageRepository, deleteStacks, emptyBucket, outputFromStack, testEnv } from './aws-helpers'; export const INTEG_TEST_DIR = path.join(os.tmpdir(), 'cdk-integ-test2'); @@ -39,6 +39,7 @@ export interface ShellOptions extends child_process.SpawnOptions { export interface CdkCliOptions extends ShellOptions { options?: string[]; + neverRequireApproval?: boolean; } export function log(x: string) { @@ -48,8 +49,10 @@ export function log(x: string) { export async function cdkDeploy(stackNames: string | string[], options: CdkCliOptions = {}) { stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames; + const neverRequireApproval = options.neverRequireApproval ?? true; + return await cdk(['deploy', - '--require-approval=never', // We never want a prompt in an unattended test + ...(neverRequireApproval ? ['--require-approval=never'] : []), // Default to no approval in an unattended test ...(options.options ?? []), ...fullStackName(stackNames)], options); } @@ -152,6 +155,10 @@ export async function cleanup(): Promise { const bucketNames = stacksToDelete.map(stack => outputFromStack('BucketName', stack)).filter(defined); await Promise.all(bucketNames.map(emptyBucket)); + // Bootstrap stacks have ECR repositories with images which should be deleted + const imageRepositoryNames = stacksToDelete.map(stack => outputFromStack('ImageRepositoryName', stack)).filter(defined); + await Promise.all(imageRepositoryNames.map(deleteImageRepository)); + await deleteStacks(...stacksToDelete.map(s => s.StackName)); // We might have leaked some buckets by upgrading the bootstrap stack. Be @@ -206,7 +213,7 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom if (code === 0 || options.allowErrExit) { resolve((Buffer.concat(stdout).toString('utf-8') + Buffer.concat(stderr).toString('utf-8')).trim()); } else { - reject(new Error(`'${command.join(' ')}' exited with error code ${code}`)); + reject(new Error(`'${command.join(' ')}' exited with error code ${code}: ${Buffer.concat(stderr).toString('utf-8').trim()}`)); } }); }); diff --git a/packages/aws-cdk/test/integ/cli/cli.integtest.ts b/packages/aws-cdk/test/integ/cli/cli.integtest.ts index 731f901d90a0d..b31bbf314d60a 100644 --- a/packages/aws-cdk/test/integ/cli/cli.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/cli.integtest.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import { cloudFormation, iam, lambda, retry, sleep, sns, sts, testEnv } from './aws-helpers'; import { cdk, cdkDeploy, cdkDestroy, cleanup, cloneDirectory, fullStackName, INTEG_TEST_DIR, log, prepareAppFixture, shell, STACK_NAME_PREFIX } from './cdk-helpers'; +import { integTest } from './test-helpers'; jest.setTimeout(600 * 1000); @@ -19,7 +20,7 @@ afterEach(async () => { await cleanup(); }); -test('VPC Lookup', async () => { +integTest('VPC Lookup', async () => { log('Making sure we are clean before starting.'); await cdkDestroy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' }}); @@ -31,26 +32,26 @@ test('VPC Lookup', async () => { await cdkDeploy('import-vpc', { modEnv: { ENABLE_VPC_TESTING: 'IMPORT' }}); }); -test('Two ways of shoing the version', async () => { +integTest('Two ways of shoing the version', async () => { const version1 = await cdk(['version']); const version2 = await cdk(['--version']); expect(version1).toEqual(version2); }); -test('Termination protection', async () => { - await cdkDeploy('termination-protection'); +integTest('Termination protection', async () => { + const stackName = 'termination-protection'; + await cdkDeploy(stackName); // Try a destroy that should fail - await expect(cdkDestroy('termination-protection')).rejects.toThrow('exited with error'); + await expect(cdkDestroy(stackName)).rejects.toThrow('exited with error'); - await cloudFormation('updateTerminationProtection', { - EnableTerminationProtection: false, - StackName: fullStackName('termination-protection'), - }); + // Can update termination protection even though the change set doesn't contain changes + await cdkDeploy(stackName, { modEnv: { TERMINATION_PROTECTION: 'FALSE' } }); + await cdkDestroy(stackName); }); -test('cdk synth', async () => { +integTest('cdk synth', async () => { await expect(cdk(['synth', fullStackName('test-1')])).resolves.toEqual( `Resources: topic69831491: @@ -70,7 +71,7 @@ test('cdk synth', async () => { aws:cdk:path: ${STACK_NAME_PREFIX}-test-2/topic2/Resource`); }); -test('ssm parameter provider error', async () => { +integTest('ssm parameter provider error', async () => { await expect(cdk(['synth', fullStackName('missing-ssm-parameter'), '-c', 'test:ssm-parameter-name=/does/not/exist', @@ -79,7 +80,7 @@ test('ssm parameter provider error', async () => { })).resolves.toContain('SSM parameter not available in account'); }); -test('automatic ordering', async () => { +integTest('automatic ordering', async () => { // Deploy the consuming stack which will include the producing stack await cdkDeploy('order-consuming'); @@ -87,7 +88,7 @@ test('automatic ordering', async () => { await cdkDestroy('order-providing'); }); -test('context setting', async () => { +integTest('context setting', async () => { await fs.writeFile(path.join(INTEG_TEST_DIR, 'cdk.context.json'), JSON.stringify({ contextkey: 'this is the context value', })); @@ -106,7 +107,7 @@ test('context setting', async () => { } }); -test('deploy', async () => { +integTest('deploy', async () => { const stackArn = await cdkDeploy('test-2', { captureStderr: false }); // verify the number of resources in the stack @@ -116,14 +117,14 @@ test('deploy', async () => { expect(response.StackResources?.length).toEqual(2); }); -test('deploy all', async () => { +integTest('deploy all', async () => { const arns = await cdkDeploy('test-*', { captureStderr: false }); // verify that we only deployed a single stack (there's a single ARN in the output) expect(arns.split('\n').length).toEqual(2); }); -test('nested stack with parameters', async () => { +integTest('nested stack with parameters', async () => { // STACK_NAME_PREFIX is used in MyTopicParam to allow multiple instances // of this test to run in parallel, othewise they will attempt to create the same SNS topic. const stackArn = await cdkDeploy('with-nested-stack-using-parameters', { @@ -141,7 +142,7 @@ test('nested stack with parameters', async () => { expect(response.StackResources?.length).toEqual(1); }); -test('deploy without execute', async () => { +integTest('deploy without execute', async () => { const stackArn = await cdkDeploy('test-2', { options: ['--no-execute'], captureStderr: false, @@ -156,16 +157,23 @@ test('deploy without execute', async () => { expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); }); -test('security related changes without a CLI are expected to fail', async () => { +integTest('security related changes without a CLI are expected to fail', async () => { // redirect /dev/null to stdin, which means there will not be tty attached // since this stack includes security-related changes, the deployment should // immediately fail because we can't confirm the changes - await expect(cdkDeploy('iam-test', { + const stackName = 'iam-test'; + await expect(cdkDeploy(stackName, { options: ['<', '/dev/null'], // H4x, this only works because I happen to know we pass shell: true. + neverRequireApproval: false, })).rejects.toThrow('exited with error'); + + // Ensure stack was not deployed + await expect(cloudFormation('describeStacks', { + StackName: fullStackName(stackName), + })).rejects.toThrow('does not exist'); }); -test('deploy wildcard with outputs', async () => { +integTest('deploy wildcard with outputs', async () => { const outputsFile = path.join(INTEG_TEST_DIR, 'outputs', 'outputs.json'); await fs.mkdir(path.dirname(outputsFile), { recursive: true }); @@ -184,7 +192,7 @@ test('deploy wildcard with outputs', async () => { }); }); -test('deploy with parameters', async () => { +integTest('deploy with parameters', async () => { const stackArn = await cdkDeploy('param-test-1', { options: [ '--parameters', `TopicNameParam=${STACK_NAME_PREFIX}bazinga`, @@ -204,7 +212,7 @@ test('deploy with parameters', async () => { ]); }); -test('deploy with wildcard and parameters', async () => { +integTest('deploy with wildcard and parameters', async () => { await cdkDeploy('param-test-*', { options: [ '--parameters', `${STACK_NAME_PREFIX}-param-test-1:TopicNameParam=${STACK_NAME_PREFIX}bazinga`, @@ -215,7 +223,7 @@ test('deploy with wildcard and parameters', async () => { }); }); -test('deploy with parameters multi', async () => { +integTest('deploy with parameters multi', async () => { const paramVal1 = `${STACK_NAME_PREFIX}bazinga`; const paramVal2 = `${STACK_NAME_PREFIX}=jagshemash`; @@ -243,7 +251,7 @@ test('deploy with parameters multi', async () => { ]); }); -test('deploy with notification ARN', async () => { +integTest('deploy with notification ARN', async () => { const topicName = `${STACK_NAME_PREFIX}-test-topic`; const response = await sns('createTopic', { Name: topicName }); @@ -265,7 +273,7 @@ test('deploy with notification ARN', async () => { } }); -test('deploy with role', async () => { +integTest('deploy with role', async () => { const roleName = `${STACK_NAME_PREFIX}-test-role`; await deleteRole(); @@ -343,7 +351,7 @@ test('deploy with role', async () => { } }); -test('cdk diff', async () => { +integTest('cdk diff', async () => { const diff1 = await cdk(['diff', fullStackName('test-1')]); expect(diff1).toContain('AWS::SNS::Topic'); @@ -355,11 +363,11 @@ test('cdk diff', async () => { .rejects.toThrow('exited with error'); }); -test('deploy stack with docker asset', async () => { +integTest('deploy stack with docker asset', async () => { await cdkDeploy('docker'); }); -test('deploy and test stack with lambda asset', async () => { +integTest('deploy and test stack with lambda asset', async () => { const stackArn = await cdkDeploy('lambda', { captureStderr: false }); const response = await cloudFormation('describeStacks', { @@ -377,7 +385,7 @@ test('deploy and test stack with lambda asset', async () => { expect(JSON.stringify(output.Payload)).toContain('dear asset'); }); -test('cdk ls', async () => { +integTest('cdk ls', async () => { const listing = await cdk(['ls'], { captureStderr: false }); const expectedStacks = [ @@ -407,7 +415,7 @@ test('cdk ls', async () => { } }); -test('deploy stack without resource', async () => { +integTest('deploy stack without resource', async () => { // Deploy the stack without resources await cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' }}); @@ -425,23 +433,23 @@ test('deploy stack without resource', async () => { .rejects.toThrow('conditional-resource does not exist'); }); -test('IAM diff', async () => { +integTest('IAM diff', async () => { const output = await cdk(['diff', fullStackName('iam-test')]); // Roughly check for a table like this: // - // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────┬───────────┐ - // │ │ Resource │ Effect │ Action │ Principal │ Condition │ - // ├───┼─────────────────┼────────┼────────────────┼────────────────────────────┼───────────┤ - // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazon.aws.com │ │ - // └───┴─────────────────┴────────┴────────────────┴────────────────────────────┴───────────┘ + // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────-──┬───────────┐ + // │ │ Resource │ Effect │ Action │ Principal │ Condition │ + // ├───┼─────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤ + // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazonaws.com │ │ + // └───┴─────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘ expect(output).toContain('${SomeRole.Arn}'); expect(output).toContain('sts:AssumeRole'); - expect(output).toContain('ec2.amazon.aws.com'); + expect(output).toContain('ec2.amazonaws.com'); }); -test('fast deploy', async () => { +integTest('fast deploy', async () => { // we are using a stack with a nested stack because CFN will always attempt to // update a nested stack, which will allow us to verify that updates are actually // skipped unless --force is specified. @@ -472,12 +480,12 @@ test('fast deploy', async () => { } }); -test('failed deploy does not hang', async () => { +integTest('failed deploy does not hang', async () => { // this will hang if we introduce https://github.com/aws/aws-cdk/issues/6403 again. await expect(cdkDeploy('failed')).rejects.toThrow('exited with error'); }); -test('can still load old assemblies', async () => { +integTest('can still load old assemblies', async () => { const cxAsmDir = path.join(os.tmpdir(), 'cdk-integ-cx'); const testAssembliesDirectory = path.join(__dirname, 'cloud-assemblies'); @@ -512,7 +520,7 @@ test('can still load old assemblies', async () => { } }); -test('generating and loading assembly', async () => { +integTest('generating and loading assembly', async () => { const asmOutputDir = path.join(os.tmpdir(), 'cdk-integ-asm'); await shell(['rm', '-rf', asmOutputDir]); diff --git a/packages/aws-cdk/test/integ/cli/jest.setup.js b/packages/aws-cdk/test/integ/cli/jest.setup.js deleted file mode 100644 index 752a75d4f73d4..0000000000000 --- a/packages/aws-cdk/test/integ/cli/jest.setup.js +++ /dev/null @@ -1,8 +0,0 @@ -// Print a big banner before every test, much more readable output -jasmine.getEnv().addReporter({ - specStarted: currentTest => { - process.stdout.write('================================================================\n'); - process.stdout.write(`${currentTest.fullName}\n`); - process.stdout.write('================================================================\n'); - } -}); \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli/skip-tests.txt b/packages/aws-cdk/test/integ/cli/skip-tests.txt new file mode 100644 index 0000000000000..bb43b8f55b68f --- /dev/null +++ b/packages/aws-cdk/test/integ/cli/skip-tests.txt @@ -0,0 +1,8 @@ +# This file is empty on purpose. Leave it here as documentation +# and an example. +# +# Copy this file to cli-regression-patches/vX.Y.Z/skip-tests.txt +# and edit it there if you want to exclude certain tests from running +# when performing a certain version's regression tests. +# +# Put a test name on a line by itself to skip it. \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli/test-helpers.ts b/packages/aws-cdk/test/integ/cli/test-helpers.ts new file mode 100644 index 0000000000000..1aef74a6efd28 --- /dev/null +++ b/packages/aws-cdk/test/integ/cli/test-helpers.ts @@ -0,0 +1,23 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +const SKIP_TESTS = fs.readFileSync(path.join(__dirname, 'skip-tests.txt'), { encoding: 'utf-8' }).split('\n'); + +/** + * A wrapper for jest's 'test' which takes regression-disabled tests into account and prints a banner + */ +export function integTest(name: string, callback: () => A | Promise) { + const runner = shouldSkip(name) ? test.skip : test; + + runner(name, () => { + process.stdout.write('================================================================\n'); + process.stdout.write(`${name}\n`); + process.stdout.write('================================================================\n'); + + return callback(); + }); +} + +function shouldSkip(testName: string) { + return SKIP_TESTS.includes(testName); +} \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli/test.sh b/packages/aws-cdk/test/integ/cli/test.sh index 75f98aefb9380..482956df450f4 100755 --- a/packages/aws-cdk/test/integ/cli/test.sh +++ b/packages/aws-cdk/test/integ/cli/test.sh @@ -10,42 +10,17 @@ current_version=$(node -p "require('${scriptdir}/../../../package.json').version # This allows injecting different versions, not just the current one. # Useful when testing. -VERSION_UNDER_TEST=${VERSION_UNDER_TEST:-${current_version}} +export VERSION_UNDER_TEST=${VERSION_UNDER_TEST:-${current_version}} -# check if a specific test should be skiped -# from execution in the current version. -function should_skip { - test=$1 - echo $(node -p "require('${scriptdir}/../cli.exclusions.js').shouldSkip('${test}', '${VERSION_UNDER_TEST}')") -} +cd $scriptdir -# get the justification for why a test is skipped. -# this will fail if there is no justification! -function get_skip_jusitification { - test=$1 - echo $(node -p "require('${scriptdir}/../cli.exclusions.js').getJustification('${test}', '${VERSION_UNDER_TEST}')") -} - -for test in $(cd ${scriptdir} && ls test-*.sh); do - echo "============================================================================================" - - # first check this if this test should be skipped. - # this can happen when running in regression mode - # when we introduce an intentional breaking change. - skip=$(should_skip ${test}) - - if [ ${skip} == "true" ]; then - - # make sure we have a justification, this will fail if not. - jusitification="$(get_skip_jusitification ${test})" - - # skip this specific test. - echo "${test} - skipped (${jusitification})" - continue - fi - - echo "${test}" - echo "============================================================================================" - /bin/bash ${scriptdir}/${test} -done +# Install these dependencies that the tests (written in Jest) need. +# Only if we're not running from the repo, because if we are the +# dependencies have already been installed by the containing 'aws-cdk' package's +# package.json. +if ! npx --no-install jest --version; then + echo 'Looks like we need to install jest first. Hold on.' >& 2 + npm install --prefix . jest aws-sdk +fi +npx jest --runInBand --verbose "$@" \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh b/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh index a9a68d19e6001..02219e64c73f4 100755 --- a/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh +++ b/packages/aws-cdk/test/integ/test-cli-regression-against-current-code.sh @@ -70,15 +70,19 @@ download_repo ${VERSION_UNDER_TEST} # bad behvaior when using it as directory names. sanitized_version=$(sed 's/\//-/g' <<< "${VERSION_UNDER_TEST}") +# Test must be created in the same directory here because the script files liberally +# include files from '..' and they have to exist. integ_under_test=${integdir}/cli-backwards-tests-${sanitized_version} rm -rf ${integ_under_test} echo "Copying integration tests of version ${VERSION_UNDER_TEST} to ${integ_under_test} (dont worry, its gitignored)" cp -r ${temp_dir}/package/test/integ/cli ${integ_under_test} -echo "Hotpatching the test runner (can be removed after release 1.40.0)" >&2 -cp -r ${integdir}/cli/test-jest.sh ${integ_under_test} -cp -r ${integdir}/cli/jest.config.js ${integ_under_test} -cp -r ${integdir}/cli/jest.setup.js ${integ_under_test} +patch_dir="${integdir}/cli-regression-patches/${VERSION_UNDER_TEST}" +if [[ -d "$patch_dir" ]]; then + echo "Hotpatching the tests with files from $patch_dir" >&2 + cp -r "$patch_dir"/* ${integ_under_test} +fi echo "Running integration tests of version ${VERSION_UNDER_TEST} from ${integ_under_test}" -VERSION_UNDER_TEST=${VERSION_UNDER_TEST} ${integ_under_test}/test.sh +set -x +VERSION_UNDER_TEST=${VERSION_UNDER_TEST} ${integ_under_test}/test.sh "$@" diff --git a/packages/aws-cdk/test/integ/test-cli-regression-against-latest-release.sh b/packages/aws-cdk/test/integ/test-cli-regression-against-latest-release.sh index fc3e4e3b9a859..6d0133ca06108 100755 --- a/packages/aws-cdk/test/integ/test-cli-regression-against-latest-release.sh +++ b/packages/aws-cdk/test/integ/test-cli-regression-against-latest-release.sh @@ -5,4 +5,4 @@ integdir=$(cd $(dirname $0) && pwd) # run the regular regression test but pass the env variable that will # eventually instruct our runners and wrappers to install the framework # from npmjs.org rather then using the local code. -USE_PUBLISHED_FRAMEWORK_VERSION=True ${integdir}/test-cli-regression-against-current-code.sh +USE_PUBLISHED_FRAMEWORK_VERSION=True ${integdir}/test-cli-regression-against-current-code.sh "$@" diff --git a/packages/cdk-assets/lib/private/shell.ts b/packages/cdk-assets/lib/private/shell.ts index fd145cf517704..1ae57dba1b062 100644 --- a/packages/cdk-assets/lib/private/shell.ts +++ b/packages/cdk-assets/lib/private/shell.ts @@ -30,6 +30,7 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom } const stdout = new Array(); + const stderr = new Array(); // Both write to stdout and collect child.stdout.on('data', chunk => { @@ -43,6 +44,8 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom if (!options.quiet) { process.stderr.write(chunk); } + + stderr.push(chunk); }); child.once('error', reject); @@ -51,7 +54,8 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom if (code === 0) { resolve(Buffer.concat(stdout).toString('utf-8')); } else { - reject(new ProcessFailed(code, `${renderCommandLine(command)} exited with error code ${code}`)); + const out = Buffer.concat(stderr).toString('utf-8').trim(); + reject(new ProcessFailed(code, `${renderCommandLine(command)} exited with error code ${code}: ${out}`)); } }); }); diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index 30b5074892cba..043b55c56f2bd 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -48,7 +48,7 @@ "@aws-cdk/cdk-assets-schema": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "archiver": "^4.0.1", - "aws-sdk": "^2.681.0", + "aws-sdk": "^2.689.0", "glob": "^7.1.6", "yargs": "^15.3.1" }, diff --git a/packages/cdk-dasm/package.json b/packages/cdk-dasm/package.json index 3e2a90ad5fd31..67043c1b1f359 100644 --- a/packages/cdk-dasm/package.json +++ b/packages/cdk-dasm/package.json @@ -26,7 +26,7 @@ }, "license": "Apache-2.0", "dependencies": { - "codemaker": "^1.5.0", + "codemaker": "^1.6.0", "yaml": "1.10.0" }, "devDependencies": { diff --git a/packages/decdk/package.json b/packages/decdk/package.json index 46565cefab7cd..2efed31310e63 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -178,8 +178,8 @@ "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", "constructs": "^3.0.2", - "fs-extra": "^8.1.0", - "jsii-reflect": "^1.5.0", + "fs-extra": "^9.0.1", + "jsii-reflect": "^1.6.0", "jsonschema": "^1.2.6", "yaml": "1.9.2", "yargs": "^15.3.1" @@ -190,7 +190,7 @@ "@types/yaml": "1.9.7", "@types/yargs": "^15.0.5", "jest": "^25.5.4", - "jsii": "^1.5.0" + "jsii": "^1.6.0" }, "keywords": [ "aws", diff --git a/packages/monocdk-experiment/package.json b/packages/monocdk-experiment/package.json index acfe1041e24b5..6979ba08618d3 100644 --- a/packages/monocdk-experiment/package.json +++ b/packages/monocdk-experiment/package.json @@ -41,7 +41,8 @@ "exclude": [ "package-info/maturity", "jsii/java", - "jsii/python" + "jsii/python", + "jsii/dotnet" ] }, "jsii": { @@ -51,7 +52,7 @@ "outdir": "dist", "targets": { "dotnet": { - "namespace": "Amazon.CDK.MonoCDK.Experiment", + "namespace": "Amazon.CDK", "packageId": "Amazon.CDK.MonoCDK.Experiment", "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png", "versionSuffix": "-devpreview", @@ -59,7 +60,7 @@ "assemblyOriginatorKeyFile": "../../key.snk" }, "java": { - "package": "software.amazon.awscdk.monocdkexperiment", + "package": "software.amazon.awscdk.core", "maven": { "groupId": "software.amazon.awscdk", "artifactId": "monocdk-experiment", @@ -247,9 +248,9 @@ "@types/fs-extra": "^8.1.1", "@types/node": "^10.17.24", "cdk-build-tools": "0.0.0", - "fs-extra": "^9.0.0", + "fs-extra": "^9.0.1", "pkglint": "0.0.0", - "ts-node": "^8.10.1", + "ts-node": "^8.10.2", "typescript": "~3.8.3" }, "peerDependencies": { diff --git a/scripts/list-packages b/scripts/list-packages index b4d16d6d8fc52..95a2c5ecc0379 100755 --- a/scripts/list-packages +++ b/scripts/list-packages @@ -24,8 +24,7 @@ child_process.exec('lerna ls --toposort --json', { shell: true }, (error, stdout for (const module of modules) { const pkgJson = require(path.join(module.location, 'package.json')); - // MonoCDK-Experiment does its own packaging, should be handled "non-JSII style" - if (pkgJson.jsii && pkgJson.name !== 'monocdk-experiment') { + if (pkgJson.jsii) { jsiiDirectories.push(module.location); } else { nonJsiiNames.push(pkgJson.name); diff --git a/tools/awslint/package.json b/tools/awslint/package.json index 9bb6507a893f8..959ba1f0a4697 100644 --- a/tools/awslint/package.json +++ b/tools/awslint/package.json @@ -16,11 +16,11 @@ "awslint": "bin/awslint" }, "dependencies": { - "@jsii/spec": "^1.5.0", + "@jsii/spec": "^1.6.0", "camelcase": "^6.0.0", "colors": "^1.4.0", - "fs-extra": "^8.1.0", - "jsii-reflect": "^1.5.0", + "fs-extra": "^9.0.1", + "jsii-reflect": "^1.6.0", "yargs": "^15.3.1" }, "devDependencies": { diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 09a93983b382d..b8ff407bc883f 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -39,7 +39,7 @@ "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^3.0.2", + "@typescript-eslint/eslint-plugin": "^3.1.0", "@typescript-eslint/parser": "^2.19.2", "awslint": "0.0.0", "colors": "^1.4.0", @@ -47,13 +47,13 @@ "eslint-import-resolver-node": "^0.3.3", "eslint-import-resolver-typescript": "^2.0.0", "eslint-plugin-import": "^2.20.2", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "jest": "^25.5.4", - "jsii": "^1.5.0", - "jsii-pacmak": "^1.5.0", + "jsii": "^1.6.0", + "jsii-pacmak": "^1.6.0", "nodeunit": "^0.11.3", "nyc": "^15.0.1", - "ts-jest": "^26.0.0", + "ts-jest": "^26.1.0", "tslint": "^5.20.1", "typescript": "~3.8.3", "yargs": "^15.3.1", diff --git a/tools/cdk-integ-tools/package.json b/tools/cdk-integ-tools/package.json index b64577de5ad7f..9dfb2ad3eda96 100644 --- a/tools/cdk-integ-tools/package.json +++ b/tools/cdk-integ-tools/package.json @@ -38,7 +38,7 @@ "@aws-cdk/cloudformation-diff": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "aws-cdk": "0.0.0", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "yargs": "^15.3.1" }, "keywords": [ diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index 7efe4275f0dc9..8bd831d9de73e 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -266,10 +266,19 @@ export default class CodeGenerator { this.code.line('ret.node.addDependency(depResource);'); this.code.closeBlock(); + // handle Condition + this.code.line('// handle Condition'); + this.code.openBlock('if (resourceAttributes.Condition)'); + this.code.line('const condition = options.finder.findCondition(resourceAttributes.Condition);'); + this.code.openBlock('if (!condition)'); + this.code.line("throw new Error(`Resource '${id}' uses Condition '${resourceAttributes.Condition}' that doesn't exist`);"); + this.code.closeBlock(); + this.code.line('cfnOptions.condition = condition;'); + this.code.closeBlock(); + // ToDo handle: - // 1. Condition - // 2. CreationPolicy - // 3. UpdatePolicy + // 1. CreationPolicy + // 2. UpdatePolicy this.code.line('return ret;'); this.code.closeBlock(); diff --git a/tools/cfn2ts/package.json b/tools/cfn2ts/package.json index 9a5db7b2cf4f5..8a0dcf6e662a1 100644 --- a/tools/cfn2ts/package.json +++ b/tools/cfn2ts/package.json @@ -30,9 +30,9 @@ "license": "Apache-2.0", "dependencies": { "@aws-cdk/cfnspec": "0.0.0", - "codemaker": "^1.5.0", + "codemaker": "^1.6.0", "fast-json-patch": "^3.0.0-1", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "yargs": "^15.3.1" }, "devDependencies": { diff --git a/tools/pkglint/package.json b/tools/pkglint/package.json index 7a5d121594176..5ce02cb64d1df 100644 --- a/tools/pkglint/package.json +++ b/tools/pkglint/package.json @@ -42,7 +42,7 @@ "dependencies": { "case": "^1.6.3", "colors": "^1.4.0", - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "semver": "^7.2.2", "yargs": "^15.3.1" } diff --git a/tools/pkgtools/package.json b/tools/pkgtools/package.json index 7cfde15b5d3db..5a843fe0e5a6b 100644 --- a/tools/pkgtools/package.json +++ b/tools/pkgtools/package.json @@ -35,7 +35,7 @@ "pkglint": "0.0.0" }, "dependencies": { - "fs-extra": "^8.1.0", + "fs-extra": "^9.0.1", "yargs": "^15.3.1" }, "keywords": [ diff --git a/yarn.lock b/yarn.lock index dfaf923f05271..104cf74c0ae1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -529,10 +529,10 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" -"@jsii/spec@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.5.0.tgz#55a6d7395862c287cff18cf6cf8d166b715d1e49" - integrity sha512-gmqCGiAuXd8XFwy2uqqwoA0VBhADbrPuuowK7Qfy44ZIzv2gm0txlSkKA5elwRFdqlYHCAl6GYcimZemm6x/rQ== +"@jsii/spec@^1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.6.0.tgz#a93fa8eb22684a2263f70c1ae2b7143e43548149" + integrity sha512-6S863f3YQCLG00236OOT29EOqZZRFQEQcfACZ5f3Ph1PApRRndeZLsELm23MS6cCktdgdptRzaYR0HCupajBHQ== dependencies: jsonschema "^1.2.6" @@ -670,10 +670,10 @@ is-ci "^2.0.0" npmlog "^4.1.2" -"@lerna/conventional-commits@3.18.5": - version "3.18.5" - resolved "https://registry.yarnpkg.com/@lerna/conventional-commits/-/conventional-commits-3.18.5.tgz#08efd2e5b45acfaf3f151a53a3ec7ecade58a7bc" - integrity sha512-qcvXIEJ3qSgalxXnQ7Yxp5H9Ta5TVyai6vEor6AAEHc20WiO7UIdbLDCxBtiiHMdGdpH85dTYlsoYUwsCJu3HQ== +"@lerna/conventional-commits@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/conventional-commits/-/conventional-commits-3.22.0.tgz#2798f4881ee2ef457bdae027ab7d0bf0af6f1e09" + integrity sha512-z4ZZk1e8Mhz7+IS8NxHr64wyklHctCJyWpJKEZZPJiLFJ8yKto/x38O80R10pIzC0rr8Sy/OsjSH4bl0TbbgqA== dependencies: "@lerna/validation-error" "3.13.0" conventional-changelog-angular "^5.0.3" @@ -696,10 +696,10 @@ fs-extra "^8.1.0" npmlog "^4.1.2" -"@lerna/create@3.21.0": - version "3.21.0" - resolved "https://registry.yarnpkg.com/@lerna/create/-/create-3.21.0.tgz#e813832adf3488728b139e5a75c8b01b1372e62f" - integrity sha512-cRIopzKzE2vXJPmsiwCDMWo4Ct+KTmX3nvvkQLDoQNrrRK7w+3KQT3iiorbj1koD95RsVQA7mS2haWok9SIv0g== +"@lerna/create@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/create/-/create-3.22.0.tgz#d6bbd037c3dc5b425fe5f6d1b817057c278f7619" + integrity sha512-MdiQQzCcB4E9fBF1TyMOaAEz9lUjIHp1Ju9H7f3lXze5JK6Fl5NYkouAvsLgY6YSIhXMY8AHW2zzXeBDY4yWkw== dependencies: "@evocateur/pacote" "^9.6.3" "@lerna/child-process" "3.16.5" @@ -787,13 +787,13 @@ ssri "^6.0.1" tar "^4.4.8" -"@lerna/github-client@3.16.5": - version "3.16.5" - resolved "https://registry.yarnpkg.com/@lerna/github-client/-/github-client-3.16.5.tgz#2eb0235c3bf7a7e5d92d73e09b3761ab21f35c2e" - integrity sha512-rHQdn8Dv/CJrO3VouOP66zAcJzrHsm+wFuZ4uGAai2At2NkgKH+tpNhQy2H1PSC0Ezj9LxvdaHYrUzULqVK5Hw== +"@lerna/github-client@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/github-client/-/github-client-3.22.0.tgz#5d816aa4f76747ed736ae64ff962b8f15c354d95" + integrity sha512-O/GwPW+Gzr3Eb5bk+nTzTJ3uv+jh5jGho9BOqKlajXaOkMYGBELEAqV5+uARNGWZFvYAiF4PgqHb6aCUu7XdXg== dependencies: "@lerna/child-process" "3.16.5" - "@octokit/plugin-enterprise-rest" "^3.6.1" + "@octokit/plugin-enterprise-rest" "^6.0.1" "@octokit/rest" "^16.28.4" git-url-parse "^11.1.2" npmlog "^4.1.2" @@ -820,10 +820,10 @@ "@lerna/child-process" "3.16.5" semver "^6.2.0" -"@lerna/import@3.21.0": - version "3.21.0" - resolved "https://registry.yarnpkg.com/@lerna/import/-/import-3.21.0.tgz#87b08f2a2bfeeff7357c6fd8490e638d3cd5b32d" - integrity sha512-aISkL4XD0Dqf5asDaOZWu65jgj8fWUhuQseZWuQe3UfHxav69fTS2YLIngUfencaOSZVOcVCom28YCzp61YDxw== +"@lerna/import@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/import/-/import-3.22.0.tgz#1a5f0394f38e23c4f642a123e5e1517e70d068d2" + integrity sha512-uWOlexasM5XR6tXi4YehODtH9Y3OZrFht3mGUFFT3OIl2s+V85xIGFfqFGMTipMPAGb2oF1UBLL48kR43hRsOg== dependencies: "@lerna/child-process" "3.16.5" "@lerna/command" "3.21.0" @@ -1042,10 +1042,10 @@ inquirer "^6.2.0" npmlog "^4.1.2" -"@lerna/publish@3.21.0": - version "3.21.0" - resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-3.21.0.tgz#0112393125f000484c3f50caba71a547f91bd7f4" - integrity sha512-JZ+ehZB9UCQ9nqH8Ld/Yqc/If++aK/7XIubkrB9sQ5hf2GeIbmI/BrJpMgLW/e9T5bKrUBZPUvoUN3daVipA5A== +"@lerna/publish@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/publish/-/publish-3.22.0.tgz#7a3fb61026d3b7425f3b9a1849421f67d795c55d" + integrity sha512-8LBeTLBN8NIrCrLGykRu+PKrfrCC16sGCVY0/bzq9TDioR7g6+cY0ZAw653Qt/0Kr7rg3J7XxVNdzj3fvevlwA== dependencies: "@evocateur/libnpmaccess" "^3.1.2" "@evocateur/npm-registry-fetch" "^4.0.0" @@ -1068,7 +1068,7 @@ "@lerna/run-lifecycle" "3.16.2" "@lerna/run-topologically" "3.18.5" "@lerna/validation-error" "3.13.0" - "@lerna/version" "3.21.0" + "@lerna/version" "3.22.0" figgy-pudding "^3.5.1" fs-extra "^8.1.0" npm-package-arg "^6.1.0" @@ -1181,17 +1181,17 @@ dependencies: npmlog "^4.1.2" -"@lerna/version@3.21.0": - version "3.21.0" - resolved "https://registry.yarnpkg.com/@lerna/version/-/version-3.21.0.tgz#5bcc3d2de9eb8f4db18efb0d88973f9a509eccc3" - integrity sha512-nIT3u43fCNj6uSMN1dRxFnF4GhmIiOEqSTkGSjrMU+8kHKwzOqS/6X6TOzklBmCyEZOpF/fLlGqH3BZHnwLDzQ== +"@lerna/version@3.22.0": + version "3.22.0" + resolved "https://registry.yarnpkg.com/@lerna/version/-/version-3.22.0.tgz#67e1340c1904e9b339becd66429f32dd8ad65a55" + integrity sha512-6uhL6RL7/FeW6u1INEgyKjd5dwO8+IsbLfkfC682QuoVLS7VG6OOB+JmTpCvnuyYWI6fqGh1bRk9ww8kPsj+EA== dependencies: "@lerna/check-working-tree" "3.16.5" "@lerna/child-process" "3.16.5" "@lerna/collect-updates" "3.20.0" "@lerna/command" "3.21.0" - "@lerna/conventional-commits" "3.18.5" - "@lerna/github-client" "3.16.5" + "@lerna/conventional-commits" "3.22.0" + "@lerna/github-client" "3.22.0" "@lerna/gitlab-client" "3.15.0" "@lerna/output" "3.13.0" "@lerna/prerelease-id-from-version" "3.16.0" @@ -1250,10 +1250,10 @@ is-plain-object "^3.0.0" universal-user-agent "^5.0.0" -"@octokit/plugin-enterprise-rest@^3.6.1": - version "3.6.2" - resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-3.6.2.tgz#74de25bef21e0182b4fa03a8678cd00a4e67e561" - integrity sha512-3wF5eueS5OHQYuAEudkpN+xVeUsg8vYEMMenEzLphUZ7PRZ8OJtDcsreL3ad9zxXmBbaFWzLmFcdob5CLyZftA== +"@octokit/plugin-enterprise-rest@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" + integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== "@octokit/plugin-paginate-rest@^1.1.1": version "1.1.2" @@ -1507,10 +1507,10 @@ dependencies: jszip "*" -"@types/lodash@^4.14.152": - version "4.14.152" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.152.tgz#7e7679250adce14e749304cdb570969f77ec997c" - integrity sha512-Vwf9YF2x1GE3WNeUMjT5bTHa2DqgUo87ocdgTScupY2JclZ5Nn7W2RLM/N0+oreexUk8uaVugR81NnTY/jNNXg== +"@types/lodash@^4.14.155": + version "4.14.155" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.155.tgz#e2b4514f46a261fd11542e47519c20ebce7bc23a" + integrity sha512-vEcX7S7aPhsBCivxMwAANQburHBtfN9RdyXFk84IJmu2Z4Hkg1tOFgaslRiEqqvoLtbCBi6ika1EMspE+NZ9Lg== "@types/md5@^2.2.0": version "2.2.0" @@ -1595,10 +1595,10 @@ dependencies: "@types/node" "*" -"@types/sinon@^9.0.3": - version "9.0.3" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.3.tgz#c803f2ebf96db44230ce4e632235c279830edd45" - integrity sha512-NWVG++603tEDwmz5k0DwFR1hqP3iBmq5GYi6d+0KCQMQsfDEULF1D7xqZ+iXRJHeGwLVhM+Rv73uzIYuIUVlJQ== +"@types/sinon@^9.0.4": + version "9.0.4" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.4.tgz#e934f904606632287a6e7f7ab0ce3f08a0dad4b1" + integrity sha512-sJmb32asJZY6Z2u09bl0G2wglSxDlROlAejCjsnor+LzBMz17gu8IU7vKC/vWDnv9zEq2wqADHVXFjf4eE8Gdw== dependencies: "@types/sinonjs__fake-timers" "*" @@ -1658,12 +1658,12 @@ resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.3.tgz#38fb31d82ed07dea87df6bd565721d11979fd761" integrity sha512-mhdQq10tYpiNncMkg1vovCud5jQm+rWeRVz6fxjCJlY6uhDlAn9GnMSmBa2DQwqPf/jS5YR0K/xChDEh1jdOQg== -"@typescript-eslint/eslint-plugin@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.0.2.tgz#4a114a066e2f9659b25682ee59d4866e15a17ec3" - integrity sha512-ER3bSS/A/pKQT/hjMGCK8UQzlL0yLjuCZ/G8CDFJFVTfl3X65fvq2lNYqOG8JPTfrPa2RULCdwfOyFjZEMNExQ== +"@typescript-eslint/eslint-plugin@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.1.0.tgz#4ac00ecca3bbea740c577f1843bc54fa69c3def2" + integrity sha512-D52KwdgkjYc+fmTZKW7CZpH5ZBJREJKZXRrveMiRCmlzZ+Rw9wRVJ1JAmHQ9b/+Ehy1ZeaylofDB9wwXUt83wg== dependencies: - "@typescript-eslint/experimental-utils" "3.0.2" + "@typescript-eslint/experimental-utils" "3.1.0" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" @@ -1679,13 +1679,13 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/experimental-utils@3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.0.2.tgz#bb2131baede8df28ec5eacfa540308ca895e5fee" - integrity sha512-4Wc4EczvoY183SSEnKgqAfkj1eLtRgBQ04AAeG+m4RhTVyaazxc1uI8IHf0qLmu7xXe9j1nn+UoDJjbmGmuqXQ== +"@typescript-eslint/experimental-utils@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.1.0.tgz#2d5dba7c2ac2a3da3bfa3f461ff64de38587a872" + integrity sha512-Zf8JVC2K1svqPIk1CB/ehCiWPaERJBBokbMfNTNRczCbQSlQXaXtO/7OfYz9wZaecNvdSvVADt6/XQuIxhC79w== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "3.0.2" + "@typescript-eslint/typescript-estree" "3.1.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -1712,10 +1712,10 @@ semver "^6.3.0" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.0.2.tgz#67a1ce4307ebaea43443fbf3f3be7e2627157293" - integrity sha512-cs84mxgC9zQ6viV8MEcigfIKQmKtBkZNDYf8Gru2M+MhnA6z9q0NFMZm2IEzKqAwN8lY5mFVd1Z8DiHj6zQ3Tw== +"@typescript-eslint/typescript-estree@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.1.0.tgz#eaff52d31e615e05b894f8b9d2c3d8af152a5dd2" + integrity sha512-+4nfYauqeQvK55PgFrmBWFVYb6IskLyOosYEmhH3mSVhfBp9AIJnjExdgDmKWoOBHRcPM8Ihfm2BFpZf0euUZQ== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" @@ -2134,12 +2134,12 @@ aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" -aws-sdk@^2.637.0, aws-sdk@^2.681.0: - version "2.681.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.681.0.tgz#09eeedb5ca49813dfc637908abe408ae114a6824" - integrity sha512-/p8CDJ7LZvB1i4WrJrb32FUbbPdiZFZSN6FI2lv7s/scKypmuv/iJ9kpx6QWSWQZ72kJ3Njk/0o7GuVlw0jHXw== +aws-sdk@^2.637.0, aws-sdk@^2.689.0: + version "2.689.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.689.0.tgz#f8833031afd773bfc9503f8d6325186a985d019c" + integrity sha512-l9kbgZtIbR9dux4JHoxZ3vDWAfGtp34KpDDf5cwYHC5jDTTJoe6XhBBlEDSruwKh1+5DONpSZWNVhDZ6E02ojg== dependencies: - buffer "4.9.1" + buffer "4.9.2" events "1.1.1" ieee754 "1.1.13" jmespath "0.15.0" @@ -2350,10 +2350,10 @@ buffer-from@1.x, buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -buffer@4.9.1: - version "4.9.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" - integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= +buffer@4.9.2: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== dependencies: base64-js "^1.0.2" ieee754 "^1.1.4" @@ -2689,10 +2689,10 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -codemaker@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.5.0.tgz#f0bf606e96dac89c8cf6d7641d344b3c46f11bc7" - integrity sha512-M3vtGs1koOa8OjpjaFX1T92LkcuXAWHAgArwYanAN7Ptu2mmbOJCtznubIr0GnXzetukpCFnRaf777CDgfUFIg== +codemaker@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.6.0.tgz#5fa6cf121bfb4476908666b46cf9ff34a72ef49a" + integrity sha512-B8FcGhBVMfQs+a8i8VnAWZLUgsM8IU3Q+V2hrLnBXd82Tlp/uUm5K5melOJeSKCoHHaTU8y1kNLaNo6qq47etw== dependencies: camelcase "^6.0.0" decamelize "^1.2.0" @@ -4418,10 +4418,10 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.0.tgz#b6afc31036e247b2466dc99c29ae797d5d4580a3" - integrity sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g== +fs-extra@^9.0.0, fs-extra@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" + integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== dependencies: at-least-node "^1.0.0" graceful-fs "^4.2.0" @@ -4714,12 +4714,7 @@ globrex@^0.1.1: resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== - -graceful-fs@^4.2.4: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -5980,70 +5975,70 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsii-diff@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.5.0.tgz#a8400ea7ba69e619f8c44cc6a9677a2f71587fc2" - integrity sha512-6Z3ayLF1IMFLq9tSmfpozwF9F/JwEswEYSvKOhFb/vcULP5j743HbvEXoIzRNVX/xTKdpBbo7tXmFFECC3xDEw== +jsii-diff@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.6.0.tgz#a8b2cd56fd1fd77de37061c38a6434c70c235a41" + integrity sha512-m/xS549AtR/dK6crArmJeYHaJACwv+tj/koLsn2cKmPqfK2z6FcSgKjOnQH+Q2PlgsJWVUlyaVvhQlnj+W78kw== dependencies: - "@jsii/spec" "^1.5.0" + "@jsii/spec" "^1.6.0" fs-extra "^9.0.0" - jsii-reflect "^1.5.0" - log4js "^6.2.1" + jsii-reflect "^1.6.0" + log4js "^6.3.0" typescript "~3.8.3" yargs "^15.3.1" -jsii-pacmak@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.5.0.tgz#6de664237eb1f7669ed95d007f1c86a133015498" - integrity sha512-vtTi8640mCUko4cEcPA36zhLLz9IKZXRWtHhRjTH3pKk3PfKDId+jQSzHfCNFe4Gjt9SuJndIrwtvWt7dM+zQg== +jsii-pacmak@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.6.0.tgz#d25e162b16328b50e89c3927d996a277b6acb865" + integrity sha512-Ces8X36Ccyq5AZjzpznFUfV5wd0Ol0hiprJwtGHhs5vug5uJFLZxdS0hPFBFFPLiXQWwsEToWyM7PQ+xakTTpg== dependencies: - "@jsii/spec" "^1.5.0" + "@jsii/spec" "^1.6.0" clone "^2.1.2" - codemaker "^1.5.0" + codemaker "^1.6.0" commonmark "^0.29.1" escape-string-regexp "^4.0.0" fs-extra "^9.0.0" - jsii-reflect "^1.5.0" - jsii-rosetta "^1.5.0" + jsii-reflect "^1.6.0" + jsii-rosetta "^1.6.0" semver "^7.3.2" spdx-license-list "^6.2.0" xmlbuilder "^15.1.1" yargs "^15.3.1" -jsii-reflect@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.5.0.tgz#f6007bbb3b262832d93a1b5b87b1c8d570e5c2fc" - integrity sha512-+kDzb9ariTFrox1GaLfclU1Gxs1b40hr5BSBhVBzq03F+opMyTXp2gBPotsTm8Se44wcptDbTPwEmSx1ZxR+rQ== +jsii-reflect@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.6.0.tgz#dae9ea3aa04bc95a1c244051a4c4adf691849c01" + integrity sha512-JsVGJCcezNdnR4OukLNs7p6T6f3rKbGWNByE8Omvi7GfDf9c/YiVG4LggxEQaWyIZiYYqeEtBw6JtIKj3Qme5w== dependencies: - "@jsii/spec" "^1.5.0" + "@jsii/spec" "^1.6.0" colors "^1.4.0" fs-extra "^9.0.0" - oo-ascii-tree "^1.5.0" + oo-ascii-tree "^1.6.0" yargs "^15.3.1" -jsii-rosetta@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.5.0.tgz#158365c89fbc0022b821746f3500ececfc37fd21" - integrity sha512-ABR9FWLjuEMZJrY19hjec5JCwAS9k6aQMt6F2KXTh5chFwYjU/rHUMJ/IQ9kGNiiv5MDJeVokxH/CM2gERVOdA== +jsii-rosetta@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.6.0.tgz#49cf48328f29c0b88e2bec23372696b7c4eba006" + integrity sha512-eDaaIyvFcnB07j4aRS/xWBxenHE+OEW8gWLwSnv72+BsPifcS1QOkYcz/p/fTtPjNDyjtO8dcG5V4NUsy3QKdw== dependencies: - "@jsii/spec" "^1.5.0" + "@jsii/spec" "^1.6.0" commonmark "^0.29.1" fs-extra "^9.0.0" typescript "~3.8.3" xmldom "^0.3.0" yargs "^15.3.1" -jsii@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.5.0.tgz#dcf62a953bb765e0c55ba23b0711f25a349baafb" - integrity sha512-1dWN55Bttwx9zr58iSOxCkj9O99YKtzs/51FNwjGs20KNXvVfiY6ZBnUGhq57G5mSmb+NZosX71EFknoYrFoLA== +jsii@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.6.0.tgz#35a60fed491bb3e4fa2c35965f9f2bb8593f2165" + integrity sha512-g9L2xBnKCrzfMPkaioYSz8lYATYGt8SWimycq9HxfszaI0/QjKv+68E5pgTimy6EZil+2O/KguiYqlK9jNQE7A== dependencies: - "@jsii/spec" "^1.5.0" + "@jsii/spec" "^1.6.0" case "^1.6.3" colors "^1.4.0" deep-equal "^2.0.3" fs-extra "^9.0.0" - log4js "^6.2.1" + log4js "^6.3.0" semver "^7.3.2" semver-intersect "^1.4.0" sort-json "^2.0.0" @@ -6210,27 +6205,27 @@ lcov-parse@^1.0.0: resolved "https://registry.yarnpkg.com/lcov-parse/-/lcov-parse-1.0.0.tgz#eb0d46b54111ebc561acb4c408ef9363bdc8f7e0" integrity sha1-6w1GtUER68VhrLTECO+TY73I9+A= -lerna@^3.21.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.21.0.tgz#c81a0f8df45c6b7c9d3fc9fdcd0f846aca2375c6" - integrity sha512-ux8yOwQEgIXOZVUfq+T8nVzPymL19vlIoPbysOP3YA4hcjKlqQIlsjI/1ugBe6b4MF7W4iV5vS3gH9cGqBBc1A== +lerna@^3.22.0: + version "3.22.0" + resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.22.0.tgz#da14d08f183ffe6eec566a4ef3f0e11afa621183" + integrity sha512-xWlHdAStcqK/IjKvjsSMHPZjPkBV1lS60PmsIeObU8rLljTepc4Sg/hncw4HWfQxPIewHAUTqhrxPIsqf9L2Eg== dependencies: "@lerna/add" "3.21.0" "@lerna/bootstrap" "3.21.0" "@lerna/changed" "3.21.0" "@lerna/clean" "3.21.0" "@lerna/cli" "3.18.5" - "@lerna/create" "3.21.0" + "@lerna/create" "3.22.0" "@lerna/diff" "3.21.0" "@lerna/exec" "3.21.0" - "@lerna/import" "3.21.0" + "@lerna/import" "3.22.0" "@lerna/info" "3.21.0" "@lerna/init" "3.21.0" "@lerna/link" "3.21.0" "@lerna/list" "3.21.0" - "@lerna/publish" "3.21.0" + "@lerna/publish" "3.22.0" "@lerna/run" "3.21.0" - "@lerna/version" "3.21.0" + "@lerna/version" "3.22.0" import-local "^2.0.0" npmlog "^4.1.2" @@ -6424,10 +6419,10 @@ log-driver@^1.2.7: resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.7.tgz#63b95021f0702fedfa2c9bb0a24e7797d71871d8" integrity sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg== -log4js@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.2.1.tgz#fc23a3bf287f40f5b48259958e5e0ed30d558eeb" - integrity sha512-7n+Oqxxz7VcQJhIlqhcYZBTpbcQ7XsR0MUIfJkx/n3VUjkAS4iUr+4UJlhxf28RvP9PMGQXbgTUhLApnu0XXgA== +log4js@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.3.0.tgz#10dfafbb434351a3e30277a00b9879446f715bcb" + integrity sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw== dependencies: date-format "^3.0.0" debug "^4.1.1" @@ -7300,10 +7295,10 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -oo-ascii-tree@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.5.0.tgz#e462474b98910dd33fec6518629358c74845ce1a" - integrity sha512-6s+nBxOutQeDvForKX5oFUchFSDpD2KGFIkqyv4VDX0FZl79iCx8E9R4Y/7o2umjTjuK9CrBJzO0kFKNKWbZQA== +oo-ascii-tree@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.6.0.tgz#afc53c12d9bc33e658bfd3a4b128f8aeb2c97196" + integrity sha512-3JNvbe7r+qHPHbJhnQ8R8GzgSdF5sAA49gNKnJDWD/bQ9cZzSKG8qtbGPBBnwQ2wX/YCaJ4rUTs1c2Rz2sx1+w== opener@^1.5.1: version "1.5.1" @@ -9442,10 +9437,10 @@ trivial-deferred@^1.0.1: resolved "https://registry.yarnpkg.com/trivial-deferred/-/trivial-deferred-1.0.1.tgz#376d4d29d951d6368a6f7a0ae85c2f4d5e0658f3" integrity sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM= -ts-jest@^26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.0.0.tgz#957b802978249aaf74180b9dcb17b4fd787ad6f3" - integrity sha512-eBpWH65mGgzobuw7UZy+uPP9lwu+tPp60o324ASRX4Ijg8UC5dl2zcge4kkmqr2Zeuk9FwIjvCTOPuNMEyGWWw== +ts-jest@^26.1.0: + version "26.1.0" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.1.0.tgz#e9070fc97b3ea5557a48b67c631c74eb35e15417" + integrity sha512-JbhQdyDMYN5nfKXaAwCIyaWLGwevcT2/dbqRPsQeh6NZPUuXjZQZEfeLb75tz0ubCIgEELNm6xAzTe5NXs5Y4Q== dependencies: bs-logger "0.x" buffer-from "1.x" @@ -9463,21 +9458,10 @@ ts-mock-imports@^1.2.6, ts-mock-imports@^1.3.0: resolved "https://registry.yarnpkg.com/ts-mock-imports/-/ts-mock-imports-1.3.0.tgz#ed9b743349f3c27346afe5b7454ffd2bcaa2302d" integrity sha512-cCrVcRYsp84eDvPict0ZZD/D7ppQ0/JSx4ve6aEU8DjlsaWRJWV6ADMovp2sCuh6pZcduLFoIYhKTDU2LARo7Q== -ts-node@^8.0.2: - version "8.8.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.8.2.tgz#0b39e690bee39ea5111513a9d2bcdc0bc121755f" - integrity sha512-duVj6BpSpUpD/oM4MfhO98ozgkp3Gt9qIp3jGxwU2DFvl/3IRaEAvbLa8G60uS7C77457e/m5TMowjedeRxI1Q== - dependencies: - arg "^4.1.0" - diff "^4.0.1" - make-error "^1.1.1" - source-map-support "^0.5.6" - yn "3.1.1" - -ts-node@^8.10.1: - version "8.10.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.1.tgz#77da0366ff8afbe733596361d2df9a60fc9c9bd3" - integrity sha512-bdNz1L4ekHiJul6SHtZWs1ujEKERJnHs4HxN7rjTyyVOFf3HaJ6sLqe6aPG62XTzAB/63pKRh5jTSWL0D7bsvw== +ts-node@^8.0.2, ts-node@^8.10.2: + version "8.10.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.10.2.tgz#eee03764633b1234ddd37f8db9ec10b75ec7fb8d" + integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== dependencies: arg "^4.1.0" diff "^4.0.1"