diff --git a/CHANGELOG.md b/CHANGELOG.md index fad4611dfad6c..a9fe72678f70a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,82 @@ 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.94.1](https://github.com/aws/aws-cdk/compare/v1.94.0...v1.94.1) (2021-03-16) + + +### Bug Fixes + +* **s3:** Notifications fail to deploy due to incompatible node runtime ([#13624](https://github.com/aws/aws-cdk/issues/13624)) ([26bc3d4](https://github.com/aws/aws-cdk/commit/26bc3d4951a96a4bdf3e3e10464a4e3b80ed563f)) + +## [1.94.0](https://github.com/aws/aws-cdk/compare/v1.93.0...v1.94.0) (2021-03-16) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **appmesh:** Backend, backend default and Virtual Service client policies structures are being altered +* **appmesh**: you must use the backend default interface to define backend defaults in `VirtualGateway`. + The property name also changed from `backendsDefaultClientPolicy` to `backendDefaults` +* **appmesh**: you must use the backend default interface to define backend defaults in `VirtualNode`, + (the property name also changed from `backendsDefaultClientPolicy` to `backendDefaults`), + and the `Backend` class to define a backend +* **appmesh**: you can no longer attach a client policy to a `VirtualService` + +### Features + +* **appmesh:** add missing route match features ([#13350](https://github.com/aws/aws-cdk/issues/13350)) ([b71efd9](https://github.com/aws/aws-cdk/commit/b71efd9d12843ab4b495d53e565cec97d60748f3)), closes [#11645](https://github.com/aws/aws-cdk/issues/11645) +* **aws-elasticloadbalancingv2:** add protocol version for ALB TargetGroups ([#13570](https://github.com/aws/aws-cdk/issues/13570)) ([165a3d8](https://github.com/aws/aws-cdk/commit/165a3d877b7ab23f29e42e1e74ee7c5cb35b7f24)), closes [#12869](https://github.com/aws/aws-cdk/issues/12869) +* **ecs-patterns:** Add ECS deployment circuit breaker support to higher-level constructs ([#12719](https://github.com/aws/aws-cdk/issues/12719)) ([e80a98a](https://github.com/aws/aws-cdk/commit/e80a98aa8839e9b9b89701158d82b991e9ebaa65)), closes [#12534](https://github.com/aws/aws-cdk/issues/12534) [#12360](https://github.com/aws/aws-cdk/issues/12360) + + +### Bug Fixes + +* **appmesh:** Move Client Policy from Virtual Service to backend structure ([#12943](https://github.com/aws/aws-cdk/issues/12943)) ([d3f4284](https://github.com/aws/aws-cdk/commit/d3f428435976c55ca950279cfc841665fd504370)), closes [#11996](https://github.com/aws/aws-cdk/issues/11996) +* **autoscaling:** AutoScaling on percentile metrics doesn't work ([#13366](https://github.com/aws/aws-cdk/issues/13366)) ([46114bb](https://github.com/aws/aws-cdk/commit/46114bb1f4702019a8873b9162d0a9f10763bc61)), closes [#13144](https://github.com/aws/aws-cdk/issues/13144) +* **cloudwatch:** cannot create Alarms from labeled metrics that start with a digit ([#13560](https://github.com/aws/aws-cdk/issues/13560)) ([278029f](https://github.com/aws/aws-cdk/commit/278029f25b41d956091835364e5a8de91429712c)), closes [#13434](https://github.com/aws/aws-cdk/issues/13434) +* use NodeJS 14 for all packaged custom resources ([#13488](https://github.com/aws/aws-cdk/issues/13488)) ([20a2820](https://github.com/aws/aws-cdk/commit/20a2820ee4d022663fcd0928fbc0f61153ae953f)), closes [#13534](https://github.com/aws/aws-cdk/issues/13534) [#13484](https://github.com/aws/aws-cdk/issues/13484) +* **ec2:** Security Groups support all protocols ([#13593](https://github.com/aws/aws-cdk/issues/13593)) ([8c6b3eb](https://github.com/aws/aws-cdk/commit/8c6b3ebea464e27f68ffcab32857d8baec29c413)), closes [#13403](https://github.com/aws/aws-cdk/issues/13403) +* **lambda:** fromDockerBuild output is located under /asset ([#13539](https://github.com/aws/aws-cdk/issues/13539)) ([77449f6](https://github.com/aws/aws-cdk/commit/77449f61e7075fef1240fc52becb8ea60b9ea9ad)), closes [#13439](https://github.com/aws/aws-cdk/issues/13439) +* **region-info:** ap-northeast-3 data not correctly registered ([#13564](https://github.com/aws/aws-cdk/issues/13564)) ([64da84b](https://github.com/aws/aws-cdk/commit/64da84be5c60bb8132551bcc27a7ca9c7effe95d)), closes [#13561](https://github.com/aws/aws-cdk/issues/13561) + +## [1.93.0](https://github.com/aws/aws-cdk/compare/v1.92.0...v1.93.0) (2021-03-11) + + +### Features + +* **amplify-domain:** Added config for auto subdomain creation ([#13342](https://github.com/aws/aws-cdk/issues/13342)) ([4c63f09](https://github.com/aws/aws-cdk/commit/4c63f09f1e9644877eaffbe78eede3854bec08ab)) +* **appmesh:** add route retry policies ([#13353](https://github.com/aws/aws-cdk/issues/13353)) ([66f7053](https://github.com/aws/aws-cdk/commit/66f7053a6c1f5cab540e975b30f5a2c6e35df58a)), closes [#11642](https://github.com/aws/aws-cdk/issues/11642) +* **cfnspec:** cloudformation spec v30.1.0 ([#13519](https://github.com/aws/aws-cdk/issues/13519)) ([7711981](https://github.com/aws/aws-cdk/commit/7711981ea30bfdffd21dd840d676be4a2b45c9ba)) +* **codebuild:** allow setting queued timeout ([#13467](https://github.com/aws/aws-cdk/issues/13467)) ([e09250b](https://github.com/aws/aws-cdk/commit/e09250bc92c62cb8ee0a8706ce90d0e82faf2d84)), closes [#11364](https://github.com/aws/aws-cdk/issues/11364) +* **dynamodb:** custom timeout for replication operation ([#13354](https://github.com/aws/aws-cdk/issues/13354)) ([6a5a4f2](https://github.com/aws/aws-cdk/commit/6a5a4f2d9bb6b09ad0d10066200fe53bb45f0737)), closes [#10249](https://github.com/aws/aws-cdk/issues/10249) +* **ec2:** ESP and AH IPsec protocols for Security Groups ([#13471](https://github.com/aws/aws-cdk/issues/13471)) ([f5a6647](https://github.com/aws/aws-cdk/commit/f5a6647bbe1885ba86029d10550a3ffaf80b6561)), closes [#13403](https://github.com/aws/aws-cdk/issues/13403) +* **ec2:** multipart user data ([#11843](https://github.com/aws/aws-cdk/issues/11843)) ([ed94c5e](https://github.com/aws/aws-cdk/commit/ed94c5ef1b9dd3042128b0e0c5bb14b3d9c7d497)), closes [#8315](https://github.com/aws/aws-cdk/issues/8315) +* **ecr:** add imageTagMutability prop ([#10557](https://github.com/aws/aws-cdk/issues/10557)) ([c4dc3bc](https://github.com/aws/aws-cdk/commit/c4dc3bce02790903593d80b070fca81fe7b7f08c)), closes [#4640](https://github.com/aws/aws-cdk/issues/4640) +* **ecs:** ability to access tag parameter value of TagParameterContainerImage ([#13340](https://github.com/aws/aws-cdk/issues/13340)) ([e567a41](https://github.com/aws/aws-cdk/commit/e567a410d47366855ee3e6011aa096ba987b8099)), closes [#13202](https://github.com/aws/aws-cdk/issues/13202) +* **ecs:** allow users to provide a CloudMap service to associate with an ECS service ([#13192](https://github.com/aws/aws-cdk/issues/13192)) ([a7d314c](https://github.com/aws/aws-cdk/commit/a7d314c73b9473208d94bac29ad9bd8018e00204)), closes [#10057](https://github.com/aws/aws-cdk/issues/10057) +* **events:** `EventBus.grantPutEventsTo` method for granular grants ([#13429](https://github.com/aws/aws-cdk/issues/13429)) ([122a232](https://github.com/aws/aws-cdk/commit/122a232343699304d8f206d3024fcddfb2a94bc8)), closes [#11228](https://github.com/aws/aws-cdk/issues/11228) +* **events:** dead-letter queue support for CodeBuild ([#13448](https://github.com/aws/aws-cdk/issues/13448)) ([abfc0ea](https://github.com/aws/aws-cdk/commit/abfc0ea63c10d8033a529b7497cf093e318fdf12)), closes [#13447](https://github.com/aws/aws-cdk/issues/13447) +* **events:** dead-letter queue support for StepFunctions ([#13450](https://github.com/aws/aws-cdk/issues/13450)) ([0ebcb41](https://github.com/aws/aws-cdk/commit/0ebcb4160ee16f0f7ff1072a40c8951f9a983048)), closes [#13449](https://github.com/aws/aws-cdk/issues/13449) +* **events,applicationautoscaling:** schedule can be a token ([#13064](https://github.com/aws/aws-cdk/issues/13064)) ([b1449a1](https://github.com/aws/aws-cdk/commit/b1449a178b0f9a8a951c2546428f8d75c6431f0f)) +* **iam:** SAML identity provider ([#13393](https://github.com/aws/aws-cdk/issues/13393)) ([faa0c06](https://github.com/aws/aws-cdk/commit/faa0c060dad9a5045495707e28fc85f223d4db5d)), closes [#5320](https://github.com/aws/aws-cdk/issues/5320) +* **neptune:** Support IAM authentication ([#13462](https://github.com/aws/aws-cdk/issues/13462)) ([6c5b1f4](https://github.com/aws/aws-cdk/commit/6c5b1f42fb73a132d47945b529bab73557f2b9d8)), closes [#13461](https://github.com/aws/aws-cdk/issues/13461) +* **region-info:** added AppMesh ECR account for af-south-1 region ([#12814](https://github.com/aws/aws-cdk/issues/12814)) ([b3fba43](https://github.com/aws/aws-cdk/commit/b3fba43a047df61e713e8d2271d6deee7e07b716)) +* **stepfunctions-tasks:** Support calling ApiGateway REST and HTTP APIs ([#13033](https://github.com/aws/aws-cdk/issues/13033)) ([cc608d0](https://github.com/aws/aws-cdk/commit/cc608d055ffefb798ad6378ab07f36cb241897da)), closes [#11565](https://github.com/aws/aws-cdk/issues/11565) [#11566](https://github.com/aws/aws-cdk/issues/11566) [#11565](https://github.com/aws/aws-cdk/issues/11565) + + +### Bug Fixes + +* **cfn-include:** allow boolean values for string-typed properties ([#13508](https://github.com/aws/aws-cdk/issues/13508)) ([e5dab7c](https://github.com/aws/aws-cdk/commit/e5dab7cbc67c234d191c38a8b8b84b634070b15b)) +* **ec2:** fix typo's in WindowsImage constants ([#13446](https://github.com/aws/aws-cdk/issues/13446)) ([781aa97](https://github.com/aws/aws-cdk/commit/781aa97d53fdb7511c34ddde884fdcd84c3f68a6)) +* **elasticloadbalancingv2:** upgrade to v1.92.0 drops certificates on ALB if more than 2 certificates exist ([#13490](https://github.com/aws/aws-cdk/issues/13490)) ([01b94f8](https://github.com/aws/aws-cdk/commit/01b94f8aa6c88b5e676c784aec4c879acddc042f)), closes [#13332](https://github.com/aws/aws-cdk/issues/13332) [#13437](https://github.com/aws/aws-cdk/issues/13437) +* **events:** imported EventBus does not correctly register source account ([#13481](https://github.com/aws/aws-cdk/issues/13481)) ([57e5404](https://github.com/aws/aws-cdk/commit/57e540432c1446f2233a9b0c0f4caba4e9e155d9)), closes [#13469](https://github.com/aws/aws-cdk/issues/13469) +* **iam:** oidc-provider can't pull from hosts requiring SNI ([#13397](https://github.com/aws/aws-cdk/issues/13397)) ([90dbfb5](https://github.com/aws/aws-cdk/commit/90dbfb5eec19559717ac6b30f25451461027e731)) +* **iam:** policy statement tries to validate tokens ([#13493](https://github.com/aws/aws-cdk/issues/13493)) ([8d592ea](https://github.com/aws/aws-cdk/commit/8d592ea89c0eda19329d5a31517522ec02ceb874)), closes [#13479](https://github.com/aws/aws-cdk/issues/13479) +* **init:** Python init template's stack ID doesn't match other languages ([#13480](https://github.com/aws/aws-cdk/issues/13480)) ([3f1c02d](https://github.com/aws/aws-cdk/commit/3f1c02dac7a50ce7caebce1e7f8953f6e4937e6b)) +* **stepfunctions:** no validation on state machine name ([#13387](https://github.com/aws/aws-cdk/issues/13387)) ([6c3d407](https://github.com/aws/aws-cdk/commit/6c3d4071746179dde30f615602592c2523daa56e)), closes [#13289](https://github.com/aws/aws-cdk/issues/13289) + ## [1.92.0](https://github.com/aws/aws-cdk/compare/v1.91.0...v1.92.0) (2021-03-06) -* **ecs-patterns**: the `desiredCount` property stored on the above constructs will be optional, allowing them to be undefined. This is enabled through the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` feature flag. We would recommend all CDK users to set the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` flag to `true` for all of their existing applications. +* **ecs-patterns**: the `desiredCount` property stored on the above constructs will be optional, allowing them to be undefined. This is enabled through the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` feature flag. We would recommend all CDK users to set the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` flag to `true` for all of their existing applications. ### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7615d7b10db4e..fab4ce03200c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,40 +7,28 @@ and let us know if it's not up-to-date (even better, submit a PR with your corr - [Getting Started](#getting-started) - [Pull Requests](#pull-requests) - - [Pull Request Checklist](#pull-request-checklist) - - [Step 1: Open Issue](#step-1-open-issue) + - [Step 1: Find something to work on](#step-1-find-something-to-work-on) - [Step 2: Design (optional)](#step-2-design-optional) - [Step 3: Work your Magic](#step-3-work-your-magic) - [Step 4: Commit](#step-4-commit) - [Step 5: Pull Request](#step-5-pull-request) - [Step 6: Merge](#step-6-merge) - [Breaking Changes](#breaking-changes) +- [Documentation](#documentation) + - [rosetta](#rosetta) - [Tools](#tools) - - [Main build scripts](#main-build-scripts) - - [Partial build tools](#partial-build-tools) - - [Useful aliases](#useful-aliases) - [Linters](#linters) - [cfn2ts](#cfn2ts) - [scripts/foreach.sh](#scriptsforeachsh) - [Jetbrains support (WebStorm/IntelliJ)](#jetbrains-support-webstormintellij) -- [Workflows](#workflows) - - [Full clean build](#full-clean-build) - - [Full Docker build](#full-docker-build) - - [Partial build](#partial-build) - - [Partial pack](#partial-pack) - - [Quick Iteration](#quick-iteration) - [Linking against this repository](#linking-against-this-repository) - [Running integration tests in parallel](#running-integration-tests-in-parallel) - [Visualizing dependencies in a CloudFormation Template](#visualizing-dependencies-in-a-cloudformation-template) - - [Adding Dependencies](#adding-dependencies) - - [Finding dependency cycles between packages](#finding-dependency-cycles-between-packages) - - [Updating all Dependencies](#updating-all-dependencies) - - [Running CLI integration tests](#running-cli-integration-tests) - - [Changing the Cloud Assembly Schema](#changing-cloud-assembly-schema) - - [API Compatibility Checks](#api-compatibility-checks) - - [Examples](#examples) - - [Feature Flags](#feature-flags) - - [Versioning and Release](#versioning-and-release) + - [Find dependency cycles between packages](#find-dependency-cycles-between-packages) +- [Running CLI integration tests](#running-cli-integration-tests) +- [Changing the Cloud Assembly Schema](#changing-cloud-assembly-schema) +- [Feature Flags](#feature-flags) +- [Versioning and Release](#versioning-and-release) - [Troubleshooting](#troubleshooting) - [Debugging](#debugging) - [Connecting the VS Code Debugger](#connecting-the-vs-code-debugger) @@ -49,134 +37,183 @@ and let us know if it's not up-to-date (even better, submit a PR with your corr ## Getting Started -### Gitpod +The following steps describe how to set up the AWS CDK repository on your local machine. +The alternative is to use [Gitpod](https://www.gitpod.io/), a Cloud IDE for your development. +See [Gitpod section](#gitpod) on how to set up the CDK repo on Gitpod. -For setting up a local development environment, -we recommend using [Gitpod](http://gitpod.io) - -a service that allows you to spin up an in-browser -Visual Studio Code-compatible editor, -with everything set up and ready to go for CDK development. -Just click the button below to create your private workspace: - -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/aws/aws-cdk) - -This will start a new Gitpod workspace, -and immediately kick off a build of the CDK code. -Once it's done (it takes around an hour, unfortunately), -you can work on any package that you want to modify, -as described in ['Quick Iteration'](#quick-iteration) below. - -Gitpod is free for 50 hours per month - -make sure to stop your workspace when you're done -(you can always resume it later, and it won't need to run the build again). - -### Local dependencies +### Setup -If you don't want to use Gitpod, -you need to have the following SDKs and tools locally: +The following tools need to be installed on your system prior to installing the CDK: - [Node.js >= 10.13.0](https://nodejs.org/download/release/latest-v10.x/) - We recommend using a version in [Active LTS](https://nodejs.org/en/about/releases/) - ⚠️ versions `13.0.0` to `13.6.0` are not supported due to compatibility issues with our dependencies. - [Yarn >= 1.19.1, < 2](https://yarnpkg.com/lang/en/docs/install) -- [Java >= OpenJDK 8, 11, 14](https://docs.aws.amazon.com/corretto/latest/corretto-8-ug/downloads-list.html) -- [Apache Maven >= 3.6.0, < 4.0](http://maven.apache.org/install.html) - [.NET Core SDK 3.1.x](https://www.microsoft.com/net/download) - [Python >= 3.6.5, < 4.0](https://www.python.org/downloads/release/python-365/) -- [Docker >= 19.03](https://docs.docker.com/get-docker/) -The basic commands to get the repository cloned and built locally follow: +Run the following commands to clone the repository locally. ```console $ git clone https://github.com/aws/aws-cdk.git $ cd aws-cdk +$ yarn install +``` + +We recommend that you use [Visual Studio Code](https://code.visualstudio.com/) to work on the CDK. +We use `eslint` to keep our consistent in terms of style and reducing defects. We recommend installing the +the [eslint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) as well. + +### Repo Layout + +The AWS CDK is a [NPM](https://www.npmjs.com/about) project written in [typescript](https://www.typescriptlang.org/). +More specifically, it is a [monorepo managed using lerna](https://github.com/lerna/lerna#about). +If you're unfamiliar with any of these technologies, it is useful to learn about them and will make understanding the +AWS CDK codebase easier but strictly not necessary for simple contributions. + +The CDK uses [jsii](https://github.com/aws/jsii/) as its primary build system. jsii enables us to write +typescript-compliant source code and produce polyglot libraries, such as, in Java, .NET, Python and Go. + +The repo contains `packages/` directory that contains the CDK public modules. The source code for the IAM module in the +CDK can be found at the location `packages/@aws-cdk/aws-iam`. +The repo also contains the `tools/` directory that holds custom build tooling (modeled as private npm packages) +specific to the CDK. + +### Build + +The full build of the CDK takes a long time complete; 1-2 hours depending on the performance of the build machine. +However, most first time contributions will require changing only one CDK module, sometimes two. A full build of the +CDK is not required in these cases. + +If you want to work on the `@aws-cdk/aws-ec2` module, the following command will build just the EC2 module and any +necessary dependencies. + +```console +$ cd packages/@aws-cdk/aws-ec2 +$ ../../../scripts/buildup +``` + +Note: The `buildup` command is resumable. If your build fails, you can fix the issue and run `buildup --resume` to +resume. + +At this point, you can run build and test the `aws-ec2` module by running + +```console +$ cd packages/@aws-cdk/aws-ec2 $ yarn build +$ yarn test ``` -If you get compiler errors when building, a common cause is a globally installed typescript. Try uninstalling it. +However, if you wish to build the the entire repository, the following command will achieve this. +```console +cd +yarn build ``` -npm uninstall -g typescript + +You are now ready to start contributing to the CDK. See the [Pull Requests](#pull-requests) section on how to make your +changes and submit it as a pull request. + +### Pack + +As called out in the above sections, the AWS CDK uses jsii to produce polyglot targets. This means that each CDK module +produces artifact in all of its target languages. + +Packing involves generating CDK code in the various target languages and packaging them up to be published to their +respective package managers. Once in a while, these will need to be generated either to test the experience of a new +feature, or reproduce a packaging failure. + +To package a specific module, say the `@aws-cdk/aws-ec2` module: + +```console +$ cd +$ docker run --rm --net=host -it -v $PWD:$PWD -w $PWD jsii/superchain +docker$ cd packages/@aws-cdk/aws-ec2 +docker$ ../../../scripts/foreach.sh --up yarn run package +docker$ exit ``` -Alternatively, the [Full Docker build](#full-docker-build) workflow can be used so -that you don't have to worry about installing all those tools on your local machine -and instead only depend on having a working Docker install. +The `dist/` folder within each module contains the packaged up language artifacts. -## Pull Requests +## Docker Build (Alternative) -### Pull Request Checklist - -* [ ] Testing - - Unit test added (prefer not to modify an existing test, otherwise, it's probably a breaking change) - - __CLI change?:__ coordinate update of integration tests with team - - __cdk-init template change?:__ coordinated update of integration tests with team -* [ ] Docs - - __jsdocs__: All public APIs documented - - __README__: README and/or documentation topic updated - - __Design__: For significant features, design document added to `design` folder -* [ ] Title and Description - - __Change type__: title prefixed with **fix**, **feat** and module name in parens, which will appear in changelog - - __Title__: use lower-case and doesn't end with a period - - __Breaking?__: last paragraph: "BREAKING CHANGE: " - - __Issues__: Indicate issues fixed via: "**Fixes #xxx**" or "**Closes #xxx**" -* [ ] Sensitive Modules (requires 2 PR approvers) - - IAM Policy Document (in @aws-cdk/aws-iam) - - EC2 Security Groups and ACLs (in @aws-cdk/aws-ec2) - - Grant APIs (only if not based on official documentation with a reference) - ---- - -### Step 1: Open Issue - -If there isn't one already, open an issue describing what you intend to contribute. It's useful to communicate in -advance because if someone is already working in this space, it may be worth collaborating with them -instead of duplicating the effort. +Build the docker image: -### Step 2: Design (optional) +```console +$ docker build -t aws-cdk . +``` -In some cases, it is useful to seek feedback by iterating on a design document. This is useful -when you plan a big change or feature, or you want advice on what would be the best path forward. +This allows you to run the CDK in a CDK-compatible directory with a command like: -Sometimes, the GitHub issue is sufficient for such discussions, and can be sufficient to get -clarity on what you plan to do. Sometimes, a design document would work better, so people can provide -iterative feedback. +```console +$ docker run -v $(pwd):/app -w /app aws-cdk +``` -Before starting on a design, read through the [design guidelines](DESIGN_GUIDELINES.md) for general -patterns and tips. +## Gitpod (Alternative) -In such cases, use the GitHub issue description to collect **requirements** and -**use cases** for your feature. +You may also set up your local development environment using [Gitpod](http://gitpod.io) - +a service that allows you to spin up an in-browser Visual Studio Code-compatible editor, +with everything set up and ready to go for CDK development. +Just click the button below to create your private workspace: + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/aws/aws-cdk) + +This will start a new Gitpod workspace, with the CDK repository [pre-built](https://www.gitpod.io/docs/prebuilds/). +You can now work on your CDK repository, as described in the [Getting Started](#getting-started) section. -Then, create a design document in markdown format under the `design/` directory -and request feedback through a pull request. Prefix the PR title with "**RFC:**" -(request for comments). +Gitpod is free for 50 hours per month - make sure to stop your workspace when you're done +(you can always resume it later, and it won't need to run the build again). -Once the design is finalized, you can re-purpose this PR for the implementation, -or open a new PR to that end. +For Gitpod users only! The best way to supply CDK with your AWS credentials is to add them as +[persisting environment variables](https://www.gitpod.io/docs/environment-variables). +Adding them works as follows via terminal: + +```shell +eval $(gp env -e AWS_ACCESS_KEY_ID=XXXXXXXXX) +eval $(gp env -e AWS_SECRET_ACCESS_KEY=YYYYYYY) +eval $(gp env -e AWS_DEFAULT_REGION=ZZZZZZZZ) +eval $(gp env -e) +``` + +## Pull Requests + +### Step 1: Find something to work on + +If you want to contribute a specific feature or fix you have in mind, look at active [pull +requests](https://github.com/aws/aws-cdk/pulls) to see if someone else is already working on it. If not, you can start +contributing your changes. + +On the other hand, if you are here looking for an issue to work on, explore our [backlog of +issues](https://github.com/aws/aws-cdk/issues) and find something that piques your interest. We have labeled all of our +issues for easy filtration. +If you are looking for your first contribution, the ['good first issue' +label](https://github.com/aws/aws-cdk/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) will be of help. + +### Step 2: Design (optional) + +In some cases, it is useful to seek feedback by iterating on a design document. This is useful +when you plan a big change or feature, or you want advice on what would be the best path forward. + +In most cases, the GitHub issue is sufficient for such discussions, and can be sufficient to get +clarity on what you plan to do. If the changes are significant or intrusive to the existing CDK experience, +consider writing an RFC in our [RFC repository](https://github.com/aws/aws-cdk-rfcs) before jumping into our code base. ### Step 3: Work your Magic Work your magic. Here are some guidelines: -* Coding style (abbreviated): - * In general, follow the style of the code around you - * 2 space indentation - * 120 characters wide - * ATX style headings in markdown (e.g. `## H2 heading`) +* Coding style. + * If your change introduces a new construct, take a look at our [design guidelines](./docs/DESIGN_GUIDELINES.md) for + construct libraries. + We also have an [example construct library](packages/@aws-cdk/example-construct-library) that showcases a simple + construct library with a single construct. + * We have a number of linters that run during standard build that will enforce coding consistency and correctness. + Watch out for their error messages and adjust your code accordingly. * Every change requires a unit test * If you change APIs, make sure to update the module's README file * Try to maintain a single feature/bugfix per pull request. It's okay to introduce a little bit of housekeeping - changes along the way, but try to avoid conflating multiple features. Eventually, all these are going to go into a - single commit, so you can use that to frame your scope. -* If your change introduces a new construct, take a look at the our - [example Construct Library](packages/@aws-cdk/example-construct-library) for an explanation of the common patterns we use. - Feel free to start your contribution by copy&pasting files from that project, - and then edit and rename them as appropriate - - it might be easier to get started that way. -* If your change includes code examples (in the `README.md` file or as part of regular TSDoc tags), - you should probably validate those examples can be successfully compiled and trans-literated by - running `yarn rosetta:extract` (this requires other packages used by code examples are built). + changes along the way, but try to avoid conflating multiple features. Eventually, all these are going to go into a + single commit, so you can use that to frame your scope. #### Integration Tests @@ -189,17 +226,6 @@ Integration tests perform a few functions in the CDK code base - 3. (Optionally) Acts as a way to validate that constructs set up the CloudFormation resources as expected. A successful CloudFormation deployment does not mean that the resources are set up correctly. -For Gitpod users only! The best way to supply CDK with your AWS credentials is to add them as -[persisting environment variables](https://www.gitpod.io/docs/environment-variables). -Adding them works as follows via terminal: - -```shell -eval $(gp env -e AWS_ACCESS_KEY_ID=XXXXXXXXX) -eval $(gp env -e AWS_SECRET_ACCESS_KEY=YYYYYYY) -eval $(gp env -e AWS_DEFAULT_REGION=ZZZZZZZZ) -eval $(gp env -e) -``` - If you are working on a new feature that is using previously unused CloudFormation resource types, or involves configuring resource types across services, you need to write integration tests that use these resource types or features. @@ -220,6 +246,20 @@ Examples: * [integ.destinations.ts](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-lambda-destinations/test/integ.destinations.ts#L7) * [integ.token-authorizer.ts](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.ts#L6) +#### yarn watch (Optional) + +We've added a watch feature to the CDK that builds your code as you type it. Start this by running `yarn watch` for +each module that you are modifying. + +For example, watch the EC2 and IAM modules in a second terminal session: + +```console +$ cd packages/@aws-cdk/aws-ec2 +$ yarn watch & # runs in the background +$ cd packages/@aws-cdk/aws-iam +$ yarn watch & # runs in the background +``` + ### Step 4: Commit Create a commit with the proposed changes: @@ -250,21 +290,19 @@ 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: `/`). +* Push to a GitHub fork. + * CDK core members can push to a branch on the AWS CDK repo (naming convention: `/`). * 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 +* 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 for you when you finalize your merge commit message. * Make sure to update the PR title/description if things change. The PR title/description are going to be used as the commit title/message and will appear in the CHANGELOG, so maintain them all the way throughout the process. - - ### Step 6: Merge * Make sure your PR builds successfully (we have CodeBuild setup to automatically build all PRs). -* Once approved and tested, a maintainer will squash-merge to master and will use your PR title/description as the +* Once approved and tested, one of our bots will squash-merge to master and will use your PR title/description as the commit message. ## Breaking Changes @@ -275,13 +313,9 @@ programs that customers could have been writing against the current version of the CDK, that will no longer "work correctly" with the proposed new version of the CDK. -Breaking changes are not allowed in *stable* libraries¹. They are permissible -but still *highly discouraged* in experimental libraries, and require explicit -callouts in the bodies of Pull Requests that introduce them. - -> ¹) Note that starting in version 2 of the CDK, the majority of library code will be -> bundled into a single main CDK library which will be considered stable, and so -> no code in there can undergo breaking changes. +Breaking changes are not allowed in *stable* libraries. They are permitted +in experimental libraries, unless the maintainer of the module decides that it should be avoided. +Breaking changes require explicit callouts in the bodies of Pull Requests that introduce them. Breaking changes come in two flavors: @@ -327,8 +361,11 @@ $ yarn build $ yarn compat ``` -To figure out if the changes you made were breaking. See the section [API Compatibility -Checks](#api-compatibility-checks) for more information. +The only case where it is legitimate to break a public API is if the existing +API is a bug that blocked the usage of a feature. This means that by breaking +this API we will not break anyone, because they weren't able to use it. The file +`allowed-breaking-changes.txt` in the root of the repo is an exclusion file that +can be used in these cases. #### Dealing with breaking API surface changes @@ -404,57 +441,134 @@ If the new behavior is going to be breaking, the user must opt in to it, either Of these two, the first one is preferred if possible (as feature flags have non-local effects which can cause unintended effects). -## Tools +## Documentation + +Every module's README is rendered as the landing page of the official documentation. For example, this is +the README for the `aws-ec2` module - https://docs.aws.amazon.com/cdk/api/latest/docs/aws-ec2-readme.html. + +### Rosetta + +The README file contains code snippets written as typescript code. Code snippets typed in fenced code blocks +(such as `` ```ts ``) will be automatically extracted, compiled and translated to other languages when the +during the [pack](#pack) step. We call this feature 'rosetta'. -The CDK is a big project, and at the moment, all of the CDK modules are mastered in a single monolithic repository -(uses [lerna](https://github.com/lerna/lerna)). There are pros and cons to this approach, and it's especially valuable -to maintain integrity in the early stage of the project where things constantly change across the stack. In the future, -we believe many of these modules will be extracted to their own repositories. +You can run rosetta on the EC2 module (or any other module) by running: -Another complexity is that the CDK is packaged using [jsii](https://github.com/aws/jsii) to multiple programming -languages. This means that when a full build is complete, there will be a version of each module for each supported -language. +```console +$ cd packages/@aws-cdk/aws-ec2 +$ yarn rosetta:extract --strict +``` -However, in many cases, you can probably get away with just building a portion of the project, based on areas that you -want to work on. +To successfully do that, they must be compilable. The easiest way to do that is using +a *fixture*, which looks like this: -We recommend that you use [Visual Studio Code](https://code.visualstudio.com/) to work on the CDK. Be sure to install -the [eslint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) for it as well, since we have -strict linting rules that will prevent your code from compiling, but with VSCode and this extension can be automatically -fixed for you by hitting `Ctrl-.` when your cursor is on a red underline. +```` +```ts fixture=with-bucket +bucket.addLifecycleTransition({ ...props }); +``` +```` -### Main build scripts +While processing the examples, the tool will look for a file called +`rosetta/with-bucket.ts-fixture` in the package directory. This file will be +treated as a regular TypeScript source file, but it must also contain the text +`/// here`, at which point the example will be inserted. The complete file must +compile properly. -The build process is divided into stages, so you can invoke them as needed from the root of the repo: +Before the `/// here` marker, the fixture should import the necessary packages +and initialize the required variables. -- __`yarn build`__: runs the `build` and `test` commands in all modules (in topological order). -- __`yarn pack`__: packages all modules to all supported languages and produces a `dist/` directory with all the outputs - (running this script requires that you installed the [toolchains](#getting-started) for all target languages on your - system). +When no fixture is specified, the fixture with the name +`rosetta/default.ts-fixture` will be used if present. `nofixture` can be used to +opt out of that behavior. -### Partial build tools +In an `@example` block, which is unfenced, the first line of the example can +contain three slashes to achieve the same effect: -There are also two useful scripts in the `scripts` directory that can help you build part of the repo: +``` +/** + * @example + * /// fixture=with-bucket + * bucket.addLifecycleTransition({ ...props }); + */ +``` -- __`scripts/buildup`__: builds the current module and all of its dependencies (in topological order). -- __`scripts/builddown`__: builds the current module and all of its consumers (in topological order). +For a practical example of how making sample code compilable works, see the +`aws-ec2` package. -### Useful aliases +#### Recommendations -You can also add a few useful aliases to your shell profile: +In order to offer a consistent documentation style throughout the AWS CDK +codebase, example code should follow the following recommendations (there may be +cases where some of those do not apply - good judgement is to be applied): -```bash -# runs an npm script via lerna for a the current module -alias lr='lerna run --stream --scope $(node -p "require(\"./package.json\").name")' +- Types from the documented module should be **un-qualified**: -# runs "yarn build" (build + test) for the current module -alias lb='lr build' -alias lt='lr test' + ```ts + // An example in the @aws-cdk/core library, which defines Duration + Duration.minutes(15); + ``` + +- Types from other modules should be **qualified**: -# runs "yarn watch" for the current module (recommended to run in a separate terminal session): -alias lw='lr watch' + ```ts + // An example in the @aws-cdk/core library, using something from @aws-cdk/aws-s3 + const bucket = new s3.Bucket(this, 'Bucket'); + // ...rest of the example... + ``` + +- Within `.ts-fixture` files, make use of `declare` statements instead of + writing a compatible value (this will make your fixtures more durable): + + ```ts + // An hypothetical 'rosetta/default.ts-fixture' file in `@aws-cdk/core` + import * as kms from '@aws-cdk/aws-kms'; + import * as s3 from '@aws-cdk/aws-s3'; + import { StackProps } from '@aws-cdk/core'; + + declare const kmsKey: kms.IKey; + declare const bucket: s3.Bucket; + + declare const props: StackProps; + ``` + +## Tools (Advanced) + +### scripts/foreach.sh + +This wonderful tool allows you to execute a command for all modules in this repo +in topological order, but has the incredible property of being stateful. This +means that if a command fails, you can fix the issue and resume from where you +left off. + +To start a session, run: + +```console +$ scripts/foreach.sh COMMAND +``` + +This will execute "COMMAND" for each module in the repo (cwd will be the directory of the module). +If a task fails, it will stop. To resume, simply run `foreach.sh` again (with or without the same command). + +To reset the session (either when all tasks finished or if you wish to run a different session), run: + +```console +$ scripts/foreach.sh --reset +``` + +If you wish to run a command only against a module's dependency closure, use: + +```console +$ cd packages/my-module +$ ../scripts/foreach.sh --up COMMAND ``` +This will execute `COMMAND` against `my-module` and all its deps (in a topological order, of course). + +Consequently, there are two useful scripts that are built on top of `foreach.sh`, and lets you build modules. + +- __`scripts/buildup`__: builds the current module and all of its dependencies (in topological order). +- __`scripts/builddown`__: builds the current module and all of its consumers (in topological order). + ### Linters All linters are executed automatically as part of the build script, `yarn build`. @@ -501,14 +615,14 @@ $ lr pkglint **awslint** is a linter for the AWS Construct Library APIs. It is executed as a part of the build of all AWS modules in the project and enforces the [AWS -Construct Library Design Guidelines](./DESIGN_GUIDELINES.md). +Construct Library Design Guidelines](./docs/DESIGN_GUIDELINES.md). For more information about this tool, see the [awslint README](./packages/awslint/README.md). Generally speaking, if you make any changes which violate an awslint rule, build will fail with appropriate messages. All rules are documented and explained in -the [guidelines](./DESIGN_GUIDELINES.md). +the [guidelines](./docs/DESIGN_GUIDELINES.md). Here are a few useful commands: @@ -520,31 +634,6 @@ Here are a few useful commands: evaluate only the rule specified [awslint README](./packages/awslint/README.md) for details on include/exclude rule patterns. - -#### jsii-rosetta - -**jsii-rosetta** can be used to verify that all code examples included in documentation for a package (including those -in `README.md`) successfully compile against the library they document. It is recommended to run it to ensure all -examples are still accurate. Successfully building examples is also necessary to ensure the best possible translation to -other supported languages (`C#`, `Java`, `Python`, ...). - -> Note that examples may use libraries that are not part of the `dependencies` or `devDependencies` of the documented -> package. For example, `@aws-cdk/core` contains many examples that leverage libraries built *on top of it* (such as -> `@aws-cdk/aws-sns`). Such libraries must be built (using `yarn build`) before **jsii-rosetta** can verify that -> examples are correct. - -To run **jsii-rosetta** in *strict* mode (so that it always fails when encountering examples that fail to compile), use -the following command: - -```console -$ yarn rosetta:extract --strict -``` - -For more information on how you can address examples that fail compiling due to missing fixtures (declarations that are -necessary for the example to compile, but which would distract the reader away from what is being demonstrated), you -might need to introduce [rosetta fixtures](https://github.com/aws/jsii/tree/main/packages/jsii-rosetta#fixtures). Refer -to the [Examples](#examples) section. - ### cfn2ts This tool is used to generate our low-level CloudFormation resources @@ -561,37 +650,6 @@ Each module also has an npm script called `cfn2ts`: * `yarn cfn2ts`: generates L1 for a specific module * `lerna run cfn2ts`: generates L1 for the entire repo -### scripts/foreach.sh - -This wonderful tool allows you to execute a command for all modules in this repo -in topological order, but has the incredible property of being stateful. This -means that if a command fails, you can fix the issue and resume from where you -left off. - -To start a session, run: - -```console -$ scripts/foreach.sh COMMAND -``` - -This will execute "COMMAND" for each module in the repo (cwd will be the directory of the module). -If a task fails, it will stop. To resume, simply run `foreach.sh` again (with or without the same command). - -To reset the session (either when all tasks finished or if you wish to run a different session), run: - -```console -$ scripts/foreach.sh --reset -``` - -If you wish to run a command only against a module's dependency closure, use: - -```console -$ cd packages/my-module -$ ../scripts/foreach.sh --up COMMAND -``` - -This will execute `COMMAND` against `my-module` and all its deps (in a topological order, of course). - ### Jetbrains support (WebStorm/IntelliJ) This project uses lerna and utilizes symlinks inside nested `node_modules` directories. You may encounter an issue during @@ -599,146 +657,19 @@ indexing where the IDE attempts to index these directories and keeps following l available memory and crashes. To fix this, you can run ```node ./scripts/jetbrains-remove-node-modules.js``` to exclude these directories. -## Workflows - -This section includes step-by-step descriptions of common workflows. - -### Full clean build - -Clone the repo: - -```console -$ git clone https://github.com/aws/aws-cdk.git -$ cd aws-cdk -``` - -If you already have a local repo and you want a fresh build, run `git clean -fdx` from the root. - -Install and build: - -```console -$ ./install.sh -$ yarn build -``` - -If you also wish to package to all languages, make sure you have all the [toolchains](#getting-started) and now run: - -``` -$ ./pack.sh -``` - -> NOTE: in local builds, pack.sh will finish but will fail with an error -> indicating the build artifacts use the marker version (`0.0.0`). This is -> normal, and you can trust the output in `dist/` despite the failure. This is a -> protection we have to make sure we don't accidentally release artifacts with -> the marker version. - -### Full Docker build - -Clone the repo: - -```console -$ git clone https://github.com/aws/aws-cdk.git -$ cd aws-cdk -``` - -If you already have a local repo and you want a fresh build, run `git clean -fdx` from the root. - -Build the docker image: - -```console -$ docker build -t aws-cdk . -``` - -This allows you to run the CDK in a CDK-compatible directory with a command like: - -```console -$ docker run -v $(pwd):/app -w /app aws-cdk -``` - -### Partial build - -In many cases, you don't really need to build the entire project. Say you want to work on the `@aws-cdk/aws-ec2` module: - -```console -$ yarn install -$ cd packages/@aws-cdk/aws-ec2 -$ ../../../scripts/buildup -``` - -Note that `buildup` uses `foreach.sh`, which means it is resumable. If your build fails and you wish to resume, just run -`buildup --resume`. If you wish to restart, run `buildup` again. - -### Partial pack - -Packing involves generating CDK code in the various target languages and packaging them up to be published to their -respective package managers. Once in a while, these will need to be generated either to test the experience of a new -feature, or reproduce a packaging failure. - -Before running this, make sure either that the CDK module and all of its dependencies are already built. See [Partial -build](#partial-build) or [Full clean build](#full-clean-build). - -To package a specific module, say the `@aws-cdk/aws-ec2` module: - -```console -$ cd -$ docker run --rm --net=host -it -v $PWD:$PWD -w $PWD jsii/superchain -docker$ cd packages/@aws-cdk/aws-ec2 -docker$ ../../../scripts/foreach.sh --up yarn run package -docker$ exit -``` - -The `dist/` folder within each module contains the packaged up language artifacts. - -### Quick Iteration - -After you've built the modules you want to work on once, use `yarn watch` for each module that you are modifying. - -Watch the EC2 and IAM modules in a second terminal session: - -```console -$ cd packages/@aws-cdk/aws-ec2 -$ yarn watch & # runs in the background -$ cd packages/@aws-cdk/aws-iam -$ yarn watch & # runs in the background -``` - -Code... - -Now to test, you can either use `yarn test` or invoke nodeunit/jest directly: - -Running nodeunit tests directly on a module: -```console -$ cd packages/@aws-cdk/aws-iam -$ nodeunit test/test.*.js - -``` - -Running jest tests directly on a module: -```console -$ cd packages/@aws-cdk/aws-iam -$ jest test/*test.js - -``` - ### Linking against this repository -The script `./link-all.sh` can be used to generate symlinks to all modules in this repository under some `node_module` -directory. This can be used to develop against this repo as a local dependency. +If you are developing your own CDK application or library and want to use the locally checked out version of the +AWS CDK, instead of the the version of npm, the `./link-all.sh` script will help here. -One can use the `postinstall` script to symlink this repo: +This script symlinks the built modules from the local AWS CDK repo under the `node_modules/` folder of the CDK app or +library. -```json -{ - "scripts": { - "postinstall": "../aws-cdk/link-all.sh" - } -} +```console +$ cd +$ /link-all.sh ``` -This assumes this repo is a sibling of the target repo and will install the CDK as a linked dependency during -`yarn install`. - ### Running integration tests in parallel Integration tests may take a long time to complete. We can speed this up by running them in parallel @@ -760,23 +691,7 @@ Use GraphViz with `template-deps-to-dot`: $ cdk -a some.app.js synth | $awscdk/scripts/template-deps-to-dot | dot -Tpng > deps.png ``` -### Adding Dependencies - -The root [package.json](./package.json) includes global devDependencies (see -[lerna docs](https://github.com/lerna/lerna#common-devdependencies)) on the topic. - - * To add a global dependency, run `yarn add --dev` at the root. - * To add a dependency for a specific module, run `yarn add ` inside the module's directory. - -Guidelines: - - * We cannot accept dependencies that use non-permissive open source licenses (Apache, MIT, etc). - * Make sure dependencies are defined using [caret - ranges](https://docs.npmjs.com/misc/semver#caret-ranges-123-025-004) (e.g. `^1.2.3`). This enables non-breaking - updates to automatically be picked up. - * Make sure `yarn.lock` is included in your commit. - -### Finding dependency cycles between packages +### Find dependency cycles between packages You can use `find-cycles` to print a list of internal dependency cycles: @@ -792,159 +707,19 @@ Cycle: @aws-cdk/aws-sns => @aws-cdk/aws-lambda => @aws-cdk/aws-codecommit => @aw Cycle: @aws-cdk/aws-sns => @aws-cdk/aws-lambda => @aws-cdk/aws-codecommit => @aws-cdk/aws-codepipeline => @aws-cdk/aws-sns ``` -### Updating all Dependencies - -To update all dependencies (without bumping major versions): - -1. Obtain a fresh clone from "master". -2. Run `yarn install` -3. Run `./scripts/update-dependencies.sh --mode full` (use `--mode semver` to avoid bumping major versions) -4. Submit a Pull Request. - -### Running CLI integration tests +## Running CLI integration tests The CLI package (`packages/aws-cdk`) has some integration tests that aren't run as part of the regular build, since they have some particular requirements. See the [CLI CONTRIBUTING.md file](packages/aws-cdk/CONTRIBUTING.md) for more information on running those tests. -### Changing Cloud Assembly Schema +## Changing Cloud Assembly Schema If you plan on making changes to the `cloud-assembly-schema` package, make sure you familiarize yourself with its own [contribution guide](./packages/@aws-cdk/cloud-assembly-schema/CONTRIBUTING.md) -### API Compatibility Checks - -All stable APIs in the CDK go through a compatibility check during build using -the [jsii-diff] tool. This tool downloads the latest released version from npm -and verifies that the APIs in the current build have not changed in a breaking -way. - -[jsii-diff]: https://www.npmjs.com/package/jsii-diff - -Compatibility checks always run as part of a full build (`yarn build`). - -You can use `yarn compat` to run compatibility checks for all modules: - -```shell -(working directory is repo root) -$ yarn build -$ yarn compat -``` - -You can also run `compat` from individual package directories: - -```shell -$ cd packages/@aws-cdk/aws-sns -$ yarn build -$ yarn compat -``` - -The only case where it is legitimate to break a public API is if the existing -API is a bug that blocked the usage of a feature. This means that by breaking -this API we will not break anyone, because they weren't able to use it. The file -`allowed-breaking-changes.txt` in the root of the repo is an exclusion file that -can be used in these cases. - -### Examples - -#### Fixture Files - -Examples typed in fenced code blocks (looking like `'''ts`, but then with backticks -instead of regular quotes) will be automatically extracted, compiled and translated -to other languages when the bindings are generated. - -To successfully do that, they must be compilable. The easiest way to do that is using -a *fixture*, which looks like this: - -``` -'''ts fixture=with-bucket -bucket.addLifecycleTransition({ ...props }); -''' -``` - -While processing the examples, the tool will look for a file called -`rosetta/with-bucket.ts-fixture` in the package directory. This file will be -treated as a regular TypeScript source file, but it must also contain the text -`/// here`, at which point the example will be inserted. The complete file must -compile properly. - -Before the `/// here` marker, the fixture should import the necessary packages -and initialize the required variables. - -If no fixture is specified, the fixture with the name -`rosetta/default.ts-fixture` will be used if present. `nofixture` can be used to -opt out of that behavior. - -In an `@example` block, which is unfenced, the first line of the example can -contain three slashes to achieve the same effect: - -``` -/** - * @example - * /// fixture=with-bucket - * bucket.addLifecycleTransition({ ...props }); - */ -``` - -When including packages in your examples (even the package you're writing the -examples for), use the full package name (e.g. `import s3 = -require('@aws-cdk/aws-s3);`). The example will be compiled in an environment -where all CDK packages are available using their public names. In this way, -it's also possible to import packages that are not in the dependency set of -the current package. - -For a practical example of how making sample code compilable works, see the -`aws-ec2` package. - -#### Recommendations - -In order to offer a consistent documentation style throughout the AWS CDK -codebase, example code should follow the following recommendations (there may be -cases where some of those do not apply - good judgement is to be applied): - -- Types from the documented module should be **un-qualified**: - - ```ts - // An example in the @aws-cdk/core library, which defines Duration - Duration.minutes(15); - ``` - -- Types from other modules should be **qualified**: - - ```ts - // An example in the @aws-cdk/core library, using something from @aws-cdk/aws-s3 - const bucket = new s3.Bucket(this, 'Bucket'); - // ...rest of the example... - ``` - -- Within `.ts-fixture` files, make use of `declare` statements instead of - writing a compatible value (this will make your fixtures more durable): - - ```ts - // An hypothetical 'rosetta/default.ts-fixture' file in `@aws-cdk/core` - import * as kms from '@aws-cdk/aws-kms'; - import * as s3 from '@aws-cdk/aws-s3'; - import { StackProps } from '@aws-cdk/core'; - - declare const kmsKey: kms.IKey; - declare const bucket: s3.Bucket; - - declare const props: StackProps; - ``` - -> Those recommendations are not verified or enforced by automated tooling. Pull -> request reviewers may however request that new sample code is edited to meet -> those requirements as needed. - -#### Checking a single package - -Examples of all packages are extracted and compiled as part of the packaging -step. If you are working on getting rid of example compilation errors of a -single package, you can run `yarn rosetta:extract --strict` in the package's -directory (see the [**jsii-rosetta**](#jsii-rosetta) section). - -### Feature Flags +## Feature Flags Sometimes we want to introduce new breaking behavior because we believe this is the correct default behavior for the CDK. The problem of course is that breaking @@ -987,7 +762,7 @@ CDK](https://github.com/aws/aws-cdk/issues/3398) we will either remove the legacy behavior or flip the logic for all these features and then reset the `FEATURE_FLAGS` map for the next cycle. -#### CDKv2 +### Feature Flags - CDKv2 We have started working on the next version of the CDK, specifically CDKv2. This is currently being maintained on a separate branch `v2-main` whereas `master` continues to track versions `1.x`. @@ -1003,59 +778,10 @@ behaviour when flags are enabled or disabled in the two major versions. [jest helper methods]: https://github.com/aws/aws-cdk/blob/master/tools/cdk-build-tools/lib/feature-flag.ts -### Versioning and Release - -The `release.json` file at the root of the repo determines which release line -this branch belongs to. - -```js -{ - "majorVersion": 1 | 2, - "releaseType": "stable" | "alpha" | "rc" -} -``` - -To reduce merge conflicts in automatic merges between version branches, the -current version number is stored under `version.vNN.json` (where `NN` is -`majorVersion`) and changelogs are stored under `CHANGELOG.NN.md` (for -historical reasons, the changelog for 1.x is under `CHANGELOG.md`). When we -fork to a new release branch (e.g. `v2-main`), we will update `release.json` in -this branch to reflect the new version line, and this information will be used -to determine how releases are cut. - -The actual `version` field in all `package.json` files should always be `0.0.0`. -This means that local development builds will use version `0.0.0` instead of the -official version from the version file. - -#### `./bump.sh` - -This script uses [standard-version] to update the version in `version.vNN.json` -to the next version. By default it will perform a **minor** bump, but `./bump.sh -patch` can be used to perform a patch release if that's needed. - -This script will also update the relevant changelog file. - -[standard-version]: https://github.com/conventional-changelog/standard-version - -#### `scripts/resolve-version.js` - -The script evaluates evaluates the configuration in `release.json` and exports an -object like this: - -```js -{ - version: '2.0.0-alpha.1', // the current version - versionFile: 'version.v2.json', // the version file - changelogFile: 'CHANGELOG.v2.md', // changelog file name - prerelease: 'alpha', // prerelease tag (undefined for stable) - marker: '0.0.0' // version marker in package.json files -} -``` - -#### scripts/align-version.sh +## Versioning and Release -In official builds, the `scripts/align-version.sh` is used to update all -`package.json` files based on the version from `version.vNN.json`. +See [release.md](./docs/release.md) for details on how CDK versions are maintained and how +to trigger a new release ## Troubleshooting diff --git a/DESIGN_GUIDELINES.md b/docs/DESIGN_GUIDELINES.md similarity index 100% rename from DESIGN_GUIDELINES.md rename to docs/DESIGN_GUIDELINES.md diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000000000..f7f5f9612ea23 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,53 @@ +# Versioning and Release + +The `release.json` file at the root of the repo determines which release line +this branch belongs to. + +```js +{ + "majorVersion": 1 | 2, + "releaseType": "stable" | "alpha" | "rc" +} +``` + +To reduce merge conflicts in automatic merges between version branches, the +current version number is stored under `version.vNN.json` (where `NN` is +`majorVersion`) and changelogs are stored under `CHANGELOG.NN.md` (for +historical reasons, the changelog for 1.x is under `CHANGELOG.md`). When we +fork to a new release branch (e.g. `v2-main`), we will update `release.json` in +this branch to reflect the new version line, and this information will be used +to determine how releases are cut. + +The actual `version` field in all `package.json` files should always be `0.0.0`. +This means that local development builds will use version `0.0.0` instead of the +official version from the version file. + +## `./bump.sh` + +This script uses [standard-version] to update the version in `version.vNN.json` +to the next version. By default it will perform a **minor** bump, but `./bump.sh +patch` can be used to perform a patch release if that's needed. + +This script will also update the relevant changelog file. + +[standard-version]: https://github.com/conventional-changelog/standard-version + +## `scripts/resolve-version.js` + +The script evaluates evaluates the configuration in `release.json` and exports an +object like this: + +```js +{ + version: '2.0.0-alpha.1', // the current version + versionFile: 'version.v2.json', // the version file + changelogFile: 'CHANGELOG.v2.md', // changelog file name + prerelease: 'alpha', // prerelease tag (undefined for stable) + marker: '0.0.0' // version marker in package.json files +} +``` + +## scripts/align-version.sh + +In official builds, the `scripts/align-version.sh` is used to update all +`package.json` files based on the version from `version.vNN.json`. \ No newline at end of file diff --git a/link-all.sh b/link-all.sh index 6df7b1838ad56..03a94f7e12a05 100755 --- a/link-all.sh +++ b/link-all.sh @@ -26,7 +26,7 @@ for module in ${modules}; do # according to spec (we look in the bin/ directory instead of the { "scripts" # } entry in package.json but it's quite a bit easier. if [[ -d $module/bin ]]; then - for script in $(find $module/bin -perm /111); do + for script in $(find $module/bin -perm +111); do echo "${script} => node_modules/.bin/$(basename $script)" ln -fs ${script} node_modules/.bin done diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts index 73eaffe75a309..2739b3574a0bc 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts @@ -344,7 +344,7 @@ export class AppMeshExtension extends ServiceExtension { // Next update the app mesh config so that the local Envoy // proxy on this service knows how to route traffic to // nodes from the other service. - this.virtualNode.addBackend(otherAppMesh.virtualService); + this.virtualNode.addBackend(appmesh.Backend.virtualService(otherAppMesh.virtualService)); } private routeSpec(weightedTargets: appmesh.WeightedTarget[], serviceName: string): appmesh.RouteSpec { diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json index a6e83c8b6ad66..47ba97c3ee10d 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json @@ -1349,7 +1349,6 @@ "MeshName" ] }, - "RouteName": "name-route", "Spec": { "HttpRoute": { "Action": { @@ -1375,7 +1374,8 @@ "namevirtualrouterC00E1ACE", "VirtualRouterName" ] - } + }, + "RouteName": "name-route" } }, "namevirtualservice3DDDDF1E": { @@ -2218,7 +2218,6 @@ "MeshName" ] }, - "RouteName": "greeting-route", "Spec": { "HttpRoute": { "Action": { @@ -2244,7 +2243,8 @@ "greetingvirtualrouter0F898D1A", "VirtualRouterName" ] - } + }, + "RouteName": "greeting-route" } }, "greetingvirtualservice60AD3AD9": { @@ -3242,7 +3242,6 @@ "MeshName" ] }, - "RouteName": "greeter-route", "Spec": { "HttpRoute": { "Action": { @@ -3268,7 +3267,8 @@ "greetervirtualrouter193840BB", "VirtualRouterName" ] - } + }, + "RouteName": "greeter-route" } }, "greetervirtualservice6559950C": { diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json index 0f176c27b6ab7..9193f8c5f76a4 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.assign-public-ip.expected.json @@ -1044,7 +1044,7 @@ } }, "Handler": "framework.onEvent", - "Runtime": "nodejs10.x", + "Runtime": "nodejs14.x", "Timeout": 900 }, "DependsOn": [ diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json index 85c6a59e919de..3a3aaddac6fce 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json @@ -1537,7 +1537,6 @@ "MeshName" ] }, - "RouteName": "name-production-route", "Spec": { "HttpRoute": { "Action": { @@ -1563,7 +1562,8 @@ "nameproductionvirtualrouter00E3366D", "VirtualRouterName" ] - } + }, + "RouteName": "name-production-route" } }, "nameproductionvirtualservice4D49D5F6": { @@ -2061,7 +2061,6 @@ "MeshName" ] }, - "RouteName": "name-development-route", "Spec": { "HttpRoute": { "Action": { @@ -2087,7 +2086,8 @@ "namedevelopmentvirtualrouter0AE5105D", "VirtualRouterName" ] - } + }, + "RouteName": "name-development-route" } }, "namedevelopmentvirtualserviceD936E3FD": { diff --git a/packages/@aws-cdk/aws-amplify/README.md b/packages/@aws-cdk/aws-amplify/README.md index 7c15079d914d6..b8e3bd91c2306 100644 --- a/packages/@aws-cdk/aws-amplify/README.md +++ b/packages/@aws-cdk/aws-amplify/README.md @@ -122,7 +122,10 @@ mySinglePageApp.addCustomRule(amplify.CustomRule.SINGLE_PAGE_APPLICATION_REDIREC Add a domain and map sub domains to branches: ```ts -const domain = amplifyApp.addDomain('example.com'); +const domain = amplifyApp.addDomain('example.com', { + enableAutoSubdomain: true, // in case subdomains should be auto registered for branches + autoSubdomainCreationPatterns: ['*', 'pr*'], // regex for branches that should auto register subdomains +}); domain.mapRoot(master); // map master branch to domain root domain.mapSubDomain(master, 'www'); domain.mapSubDomain(dev); // sub domain prefix defaults to branch name diff --git a/packages/@aws-cdk/aws-amplify/lib/app.ts b/packages/@aws-cdk/aws-amplify/lib/app.ts index bf1d5bc3d5e89..43f8e308cb8f9 100644 --- a/packages/@aws-cdk/aws-amplify/lib/app.ts +++ b/packages/@aws-cdk/aws-amplify/lib/app.ts @@ -289,6 +289,7 @@ export class App extends Resource implements IApp, iam.IGrantable { return new Domain(this, id, { ...options, app: this, + autoSubDomainIamRole: this.grantPrincipal as iam.IRole, }); } } diff --git a/packages/@aws-cdk/aws-amplify/lib/domain.ts b/packages/@aws-cdk/aws-amplify/lib/domain.ts index 2cd4c8de7059d..ed2b16668eb11 100644 --- a/packages/@aws-cdk/aws-amplify/lib/domain.ts +++ b/packages/@aws-cdk/aws-amplify/lib/domain.ts @@ -1,3 +1,4 @@ +import * as iam from '@aws-cdk/aws-iam'; import { Lazy, Resource, IResolvable } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnDomain } from './amplify.generated'; @@ -21,6 +22,20 @@ export interface DomainOptions { * @default - use `addSubDomain()` to add subdomains */ readonly subDomains?: SubDomain[]; + + /** + * Automatically create subdomains for connected branches + * + * @default false + */ + readonly enableAutoSubdomain?: boolean; + + /** + * Branches which should automatically create subdomains + * + * @default - all repository branches ['*', 'pr*'] + */ + readonly autoSubdomainCreationPatterns?: string[]; } /** @@ -31,6 +46,12 @@ export interface DomainProps extends DomainOptions { * The application to which the domain must be connected */ readonly app: IApp; + + /** + * The IAM role with access to Route53 when using enableAutoSubdomain + * @default the IAM role from App.grantPrincipal + */ + readonly autoSubDomainIamRole?: iam.IRole; } /** @@ -106,6 +127,9 @@ export class Domain extends Resource { appId: props.app.appId, domainName, subDomainSettings: Lazy.any({ produce: () => this.renderSubDomainSettings() }, { omitEmptyArray: true }), + enableAutoSubDomain: !!props.enableAutoSubdomain, + autoSubDomainCreationPatterns: props.autoSubdomainCreationPatterns || ['*', 'pr*'], + autoSubDomainIamRole: props.autoSubDomainIamRole?.roleArn, }); this.arn = domain.attrArn; diff --git a/packages/@aws-cdk/aws-amplify/test/domain.test.ts b/packages/@aws-cdk/aws-amplify/test/domain.test.ts index ca7c211d14094..7b0f28f75837d 100644 --- a/packages/@aws-cdk/aws-amplify/test/domain.test.ts +++ b/packages/@aws-cdk/aws-amplify/test/domain.test.ts @@ -1,3 +1,4 @@ +import * as iam from '@aws-cdk/aws-iam'; import '@aws-cdk/assert/jest'; import { App, SecretValue, Stack } from '@aws-cdk/core'; import * as amplify from '../lib'; @@ -120,3 +121,113 @@ test('throws at synthesis without subdomains', () => { // THEN expect(() => app.synth()).toThrow(/The domain doesn't contain any subdomains/); }); + +test('auto subdomain all branches', () => { + // GIVEN + const stack = new Stack(); + const app = new amplify.App(stack, 'App', { + sourceCodeProvider: new amplify.GitHubSourceCodeProvider({ + owner: 'aws', + repository: 'aws-cdk', + oauthToken: SecretValue.plainText('secret'), + }), + }); + const prodBranch = app.addBranch('master'); + + // WHEN + const domain = app.addDomain('amazon.com', { + enableAutoSubdomain: true, + }); + domain.mapRoot(prodBranch); + + // THEN + expect(stack).toHaveResource('AWS::Amplify::Domain', { + EnableAutoSubDomain: true, + AutoSubDomainCreationPatterns: [ + '*', + 'pr*', + ], + AutoSubDomainIAMRole: { + 'Fn::GetAtt': [ + 'AppRole1AF9B530', + 'Arn', + ], + }, + }); +}); + +test('auto subdomain some branches', () => { + // GIVEN + const stack = new Stack(); + const app = new amplify.App(stack, 'App', { + sourceCodeProvider: new amplify.GitHubSourceCodeProvider({ + owner: 'aws', + repository: 'aws-cdk', + oauthToken: SecretValue.plainText('secret'), + }), + }); + const prodBranch = app.addBranch('master'); + + // WHEN + const domain = app.addDomain('amazon.com', { + enableAutoSubdomain: true, + autoSubdomainCreationPatterns: ['features/**'], + }); + domain.mapRoot(prodBranch); + + // THEN + expect(stack).toHaveResource('AWS::Amplify::Domain', { + EnableAutoSubDomain: true, + AutoSubDomainCreationPatterns: ['features/**'], + AutoSubDomainIAMRole: { + 'Fn::GetAtt': [ + 'AppRole1AF9B530', + 'Arn', + ], + }, + }); +}); + +test('auto subdomain with IAM role', () => { + // GIVEN + const stack = new Stack(); + const app = new amplify.App(stack, 'App', { + sourceCodeProvider: new amplify.GitHubSourceCodeProvider({ + owner: 'aws', + repository: 'aws-cdk', + oauthToken: SecretValue.plainText('secret'), + }), + role: iam.Role.fromRoleArn( + stack, + 'AmplifyRole', + `arn:aws:iam::${Stack.of(stack).account}:role/AmplifyRole`, + { mutable: false }, + ), + }); + const prodBranch = app.addBranch('master'); + + // WHEN + const domain = app.addDomain('amazon.com', { + enableAutoSubdomain: true, + autoSubdomainCreationPatterns: ['features/**'], + }); + domain.mapRoot(prodBranch); + + // THEN + expect(stack).toHaveResource('AWS::Amplify::Domain', { + EnableAutoSubDomain: true, + AutoSubDomainCreationPatterns: ['features/**'], + AutoSubDomainIAMRole: { + 'Fn::Join': [ + '', + [ + 'arn:aws:iam::', + { + Ref: 'AWS::AccountId', + }, + ':role/AmplifyRole', + ], + ], + }, + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigateway/lib/cors.ts b/packages/@aws-cdk/aws-apigateway/lib/cors.ts index feb7572779de9..d0cc6ee3e5a79 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/cors.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/cors.ts @@ -64,7 +64,7 @@ export interface CorsOptions { * Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) * can be cached. * - * To disable caching altogther use `disableCache: true`. + * To disable caching altogether use `disableCache: true`. * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age * @default - browser-specific (see reference) diff --git a/packages/@aws-cdk/aws-apigateway/lib/gateway-response.ts b/packages/@aws-cdk/aws-apigateway/lib/gateway-response.ts index 7c2aabbd0704e..0ab490a893051 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/gateway-response.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/gateway-response.ts @@ -206,7 +206,7 @@ export class ResponseType { */ public static readonly WAF_FILTERED = new ResponseType('WAF_FILTERED'); - /** A custom response type to suppport future cases. */ + /** A custom response type to support future cases. */ public static of(type: string): ResponseType { return new ResponseType(type.toUpperCase()); } diff --git a/packages/@aws-cdk/aws-apigateway/lib/resource.ts b/packages/@aws-cdk/aws-apigateway/lib/resource.ts index 49d0bf0356d80..714e99bce7a0b 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/resource.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/resource.ts @@ -282,12 +282,12 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc // prepare responseParams const integrationResponseParams: { [p: string]: string } = { }; - const methodReponseParams: { [p: string]: boolean } = { }; + const methodResponseParams: { [p: string]: boolean } = { }; for (const [name, value] of Object.entries(headers)) { const key = `method.response.header.${name}`; integrationResponseParams[key] = value; - methodReponseParams[key] = true; + methodResponseParams[key] = true; } return this.addMethod('OPTIONS', new MockIntegration({ @@ -297,7 +297,7 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc ], }), { methodResponses: [ - { statusCode: `${statusCode}`, responseParameters: methodReponseParams }, + { statusCode: `${statusCode}`, responseParameters: methodResponseParams }, ], }); diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 897bbc1a75c36..4bfdaeac0d6c9 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -174,7 +174,7 @@ export interface RestApiBaseProps { /** * Represents the props that all Rest APIs share. - * @deprecated - superceded by `RestApiBaseProps` + * @deprecated - superseded by `RestApiBaseProps` */ export interface RestApiOptions extends RestApiBaseProps, ResourceOptions { } @@ -441,7 +441,7 @@ export abstract class RestApiBase extends Resource implements IRestApi { /** * Metric for the total number API requests in a given period. * - * Default: samplecount over 5 minutes + * Default: sample count over 5 minutes */ public metricCount(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.cannedMetric(ApiGatewayMetrics.countSum, { diff --git a/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts b/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts index ad807d4a7d2d0..49b3ee19cd017 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts @@ -181,7 +181,7 @@ export class UsagePlan extends Resource { public addApiKey(apiKey: IApiKey): void { const prefix = 'UsagePlanKeyResource'; - // Postfixing apikey id only from the 2nd child, to keep physicalIds of UsagePlanKey for existing CDK apps unmodifed. + // Postfixing apikey id only from the 2nd child, to keep physicalIds of UsagePlanKey for existing CDK apps unmodified. const id = this.node.tryFindChild(prefix) ? `${prefix}:${Names.nodeUniqueId(apiKey.node)}` : prefix; new CfnUsagePlanKey(this, id, { diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index d8278a800a00f..7a71fc00491a1 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -187,7 +187,7 @@ const api = new HttpApi(stack, 'HttpProxyProdApi', { }); ``` -To associate a specifc `Stage` to a custom domain mapping - +To associate a specific `Stage` to a custom domain mapping - ```ts api.addStage('beta', { diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts index 092609125e70f..7a5082615abff 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts @@ -47,6 +47,25 @@ export interface BasicStepScalingPolicyProps { * @default No minimum scaling effect */ readonly minAdjustmentMagnitude?: number; + + /** + * How many evaluation periods of the metric to wait before triggering a scaling action + * + * Raising this value can be used to smooth out the metric, at the expense + * of slower response times. + * + * @default 1 + */ + readonly evaluationPeriods?: number; + + /** + * Aggregation to apply to all data points over the evaluation periods + * + * Only has meaning if `evaluationPeriods != 1`. + * + * @default - The statistic from the metric if applicable (MIN, MAX, AVERAGE), otherwise AVERAGE. + */ + readonly metricAggregationType?: MetricAggregationType; } export interface StepScalingPolicyProps extends BasicStepScalingPolicyProps { @@ -88,7 +107,7 @@ export class StepScalingPolicy extends Construct { this.lowerAction = new StepScalingAction(this, 'LowerPolicy', { adjustmentType, cooldown: props.cooldown, - metricAggregationType: aggregationTypeFromMetric(props.metric), + metricAggregationType: props.metricAggregationType ?? aggregationTypeFromMetric(props.metric), minAdjustmentMagnitude: props.minAdjustmentMagnitude, scalingTarget: props.scalingTarget, }); @@ -106,7 +125,7 @@ export class StepScalingPolicy extends Construct { metric: props.metric, alarmDescription: 'Lower threshold scaling alarm', comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD, - evaluationPeriods: 1, + evaluationPeriods: props.evaluationPeriods ?? 1, threshold, }); this.lowerAlarm.addAlarmAction(new StepScalingAlarmAction(this.lowerAction)); @@ -118,7 +137,7 @@ export class StepScalingPolicy extends Construct { this.upperAction = new StepScalingAction(this, 'UpperPolicy', { adjustmentType, cooldown: props.cooldown, - metricAggregationType: aggregationTypeFromMetric(props.metric), + metricAggregationType: props.metricAggregationType ?? aggregationTypeFromMetric(props.metric), minAdjustmentMagnitude: props.minAdjustmentMagnitude, scalingTarget: props.scalingTarget, }); @@ -136,7 +155,7 @@ export class StepScalingPolicy extends Construct { metric: props.metric, alarmDescription: 'Upper threshold scaling alarm', comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, - evaluationPeriods: 1, + evaluationPeriods: props.evaluationPeriods ?? 1, threshold, }); this.upperAlarm.addAlarmAction(new StepScalingAlarmAction(this.upperAction)); @@ -193,7 +212,7 @@ function aggregationTypeFromMetric(metric: cloudwatch.IMetric): MetricAggregatio case 'Maximum': return MetricAggregationType.MAXIMUM; default: - throw new Error(`Cannot only scale on 'Minimum', 'Maximum', 'Average' metrics, got ${statistic}`); + return MetricAggregationType.AVERAGE; } } diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/test.step-scaling-policy.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/test.step-scaling-policy.ts index 4474cc6a46f58..fbcf70eb49f75 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/test.step-scaling-policy.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/test.step-scaling-policy.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, SynthUtils } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike, SynthUtils } from '@aws-cdk/assert'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as cdk from '@aws-cdk/core'; import * as fc from 'fast-check'; @@ -152,6 +152,81 @@ export = { test.done(); }, + + 'step scaling from percentile metric'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const target = createScalableTarget(stack); + + // WHEN + target.scaleOnMetric('Tracking', { + metric: new cloudwatch.Metric({ namespace: 'Test', metricName: 'Metric', statistic: 'p99' }), + scalingSteps: [ + { upper: 0, change: -1 }, + { lower: 100, change: +1 }, + { lower: 500, change: +5 }, + ], + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + PolicyType: 'StepScaling', + StepScalingPolicyConfiguration: { + AdjustmentType: 'ChangeInCapacity', + MetricAggregationType: 'Average', + }, + })); + expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + ComparisonOperator: 'GreaterThanOrEqualToThreshold', + EvaluationPeriods: 1, + AlarmActions: [ + { Ref: 'TargetTrackingUpperPolicy72CEFA77' }, + ], + ExtendedStatistic: 'p99', + MetricName: 'Metric', + Namespace: 'Test', + Threshold: 100, + })); + + test.done(); + }, + + 'step scaling with evaluation period configured'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const target = createScalableTarget(stack); + + // WHEN + target.scaleOnMetric('Tracking', { + metric: new cloudwatch.Metric({ namespace: 'Test', metricName: 'Metric', statistic: 'p99' }), + scalingSteps: [ + { upper: 0, change: -1 }, + { lower: 100, change: +1 }, + { lower: 500, change: +5 }, + ], + evaluationPeriods: 10, + metricAggregationType: appscaling.MetricAggregationType.MAXIMUM, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + PolicyType: 'StepScaling', + StepScalingPolicyConfiguration: { + AdjustmentType: 'ChangeInCapacity', + MetricAggregationType: 'Maximum', + }, + })); + expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + ComparisonOperator: 'GreaterThanOrEqualToThreshold', + EvaluationPeriods: 10, + ExtendedStatistic: 'p99', + MetricName: 'Metric', + Namespace: 'Test', + Threshold: 100, + })); + + test.done(); + }, }; /** diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index c400cbb0af05d..678bbe22a2c20 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -186,9 +186,11 @@ const node = new VirtualNode(this, 'node', { idle: cdk.Duration.seconds(5), }, })], - backendsDefaultClientPolicy: appmesh.ClientPolicy.fileTrust({ - certificateChain: '/keys/local_cert_chain.pem', - }), + backendDefaults: { + clientPolicy: appmesh.ClientPolicy.fileTrust({ + certificateChain: '/keys/local_cert_chain.pem', + }), + }, accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), }); @@ -230,14 +232,14 @@ const virtualService = new appmesh.VirtualService(stack, 'service-1', { }), }); -node.addBackend(virtualService); +node.addBackend(appmesh.Backend.virtualService(virtualService)); ``` The `listeners` property can be left blank and added later with the `node.addListener()` method. The `healthcheck` and `timeout` properties are optional but if specifying a listener, the `port` must be added. The `backends` property can be added with `node.addBackend()`. We define a virtual service and add it to the virtual node to allow egress traffic to other node. -The `backendsDefaultClientPolicy` property are added to the node while creating the virtual node. These are virtual node's service backends client policy defaults. +The `backendDefaults` property are added to the node while creating the virtual node. These are virtual node's default settings for all backends. ## Adding TLS to a listener @@ -298,6 +300,30 @@ router.addRoute('route-http', { }); ``` +Add an HTTP2 route that matches based on method, scheme and header: + +```ts +router.addRoute('route-http2', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [ + { + virtualNode: node, + }, + ], + match: { + prefixPath: '/', + method: appmesh.HttpRouteMatchMethod.POST, + protocol: appmesh.HttpRouteProtocol.HTTPS, + headers: [ + // All specified headers must match for the route to match. + appmesh.HttpHeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HttpHeaderMatch.valueIsNot('Content-Type', 'application/json'), + ] + }, + }), +}); +``` + Add a single route with multiple targets and split traffic 50/50 ```ts @@ -320,6 +346,50 @@ router.addRoute('route-http', { }); ``` +Add an http2 route with retries: + +```ts +router.addRoute('route-http2-retry', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode: node }], + retryPolicy: { + // Retry if the connection failed + tcpRetryEvents: [appmesh.TcpRetryEvent.CONNECTION_ERROR], + // Retry if HTTP responds with a gateway error (502, 503, 504) + httpRetryEvents: [appmesh.HttpRetryEvent.GATEWAY_ERROR], + // Retry five times + retryAttempts: 5, + // Use a 1 second timeout per retry + retryTimeout: cdk.Duration.seconds(1), + }, + }), +}); +``` + +Add a gRPC route with retries: + +```ts +router.addRoute('route-grpc-retry', { + routeSpec: appmesh.RouteSpec.grpc({ + weightedTargets: [{ virtualNode: node }], + match: { serviceName: 'servicename' }, + retryPolicy: { + tcpRetryEvents: [appmesh.TcpRetryEvent.CONNECTION_ERROR], + httpRetryEvents: [appmesh.HttpRetryEvent.GATEWAY_ERROR], + // Retry if gRPC responds that the request was cancelled, a resource + // was exhausted, or if the service is unavailable + grpcRetryEvents: [ + appmesh.GrpcRetryEvent.CANCELLED, + appmesh.GrpcRetryEvent.RESOURCE_EXHAUSTED, + appmesh.GrpcRetryEvent.UNAVAILABLE, + ], + retryAttempts: 5, + retryTimeout: cdk.Duration.seconds(1), + }, + }), +}); +``` + The _RouteSpec_ class provides an easy interface for defining new protocol specific route specs. The `tcp()`, `http()` and `http2()` methods provide the spec necessary to define a protocol specific spec. @@ -369,10 +439,12 @@ const gateway = new appmesh.VirtualGateway(stack, 'gateway', { interval: cdk.Duration.seconds(10), }, })], - backendsDefaultClientPolicy: appmesh.ClientPolicy.acmTrust({ - certificateAuthorities: [acmpca.CertificateAuthority.fromCertificateAuthorityArn(stack, 'certificate', certificateAuthorityArn)], - ports: [8080, 8081], - }), + backendDefaults: { + clientPolicy: appmesh.ClientPolicy.acmTrust({ + certificateAuthorities: [acmpca.CertificateAuthority.fromCertificateAuthorityArn(stack, 'certificate', certificateAuthorityArn)], + ports: [8080, 8081], + }), + }, accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), virtualGatewayName: 'virtualGateway', }); @@ -396,7 +468,7 @@ const gateway = mesh.addVirtualGateway('gateway', { The listeners field can be omitted which will default to an HTTP Listener on port 8080. A gateway route can be added using the `gateway.addGatewayRoute()` method. -The `backendsDefaultClientPolicy` property are added to the node while creating the virtual gateway. These are virtual gateway's service backends client policy defaults. +The `backendDefaults` property is added to the node while creating the virtual gateway. These are virtual gateway's default settings for all backends. ## Adding a Gateway Route diff --git a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts index ae0f8baf53ede..bcfc066d51bea 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts @@ -1,3 +1,4 @@ +import { Duration } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnRoute } from './appmesh.generated'; import { Protocol, HttpTimeout, GrpcTimeout, TcpTimeout } from './shared-interfaces'; @@ -31,6 +32,255 @@ export interface HttpRouteMatch { * and you want the route to match requests to my-service.local/metrics, your prefix should be /metrics. */ readonly prefixPath: string; + + /** + * Specifies the client request headers to match on. All specified headers + * must match for the route to match. + * + * @default - do not match on headers + */ + readonly headers?: HttpHeaderMatch[]; + + /** + * The HTTP client request method to match on. + * + * @default - do not match on request method + */ + readonly method?: HttpRouteMatchMethod; + + /** + * The client request protocol to match on. Applicable only for HTTP2 routes. + * + * @default - do not match on HTTP2 request protocol + */ + readonly protocol?: HttpRouteProtocol; +} + +/** + * Supported values for matching routes based on the HTTP request method + */ +export enum HttpRouteMatchMethod { + /** + * GET request + */ + GET = 'GET', + + /** + * HEAD request + */ + HEAD = 'HEAD', + + /** + * POST request + */ + POST = 'POST', + + /** + * PUT request + */ + PUT = 'PUT', + + /** + * DELETE request + */ + DELETE = 'DELETE', + + /** + * CONNECT request + */ + CONNECT = 'CONNECT', + + /** + * OPTIONS request + */ + OPTIONS = 'OPTIONS', + + /** + * TRACE request + */ + TRACE = 'TRACE', + + /** + * PATCH request + */ + PATCH = 'PATCH', +} + +/** + * Supported :scheme options for HTTP2 + */ +export enum HttpRouteProtocol { + /** + * Match HTTP requests + */ + HTTP = 'http', + + /** + * Match HTTPS requests + */ + HTTPS = 'https', +} + +/** + * Configuration for `HeaderMatch` + */ +export interface HttpHeaderMatchConfig { + /** + * The HTTP route header. + */ + readonly httpRouteHeader: CfnRoute.HttpRouteHeaderProperty; +} + +/** + * Used to generate header matching methods. + */ +export abstract class HttpHeaderMatch { + /** + * The value of the header with the given name in the request must match the + * specified value exactly. + * + * @param headerName the name of the HTTP header to match against + * @param headerValue The exact value to test against + */ + static valueIs(headerName: string, headerValue: string): HttpHeaderMatch { + return new HeaderMatchImpl(headerName, false, { exact: headerValue }); + } + + /** + * The value of the header with the given name in the request must not match + * the specified value exactly. + * + * @param headerName the name of the HTTP header to match against + * @param headerValue The exact value to test against + */ + static valueIsNot(headerName: string, headerValue: string): HttpHeaderMatch { + return new HeaderMatchImpl(headerName, true, { exact: headerValue }); + } + + /** + * The value of the header with the given name in the request must start with + * the specified characters. + * + * @param headerName the name of the HTTP header to match against + * @param prefix The prefix to test against + */ + static valueStartsWith(headerName: string, prefix: string): HttpHeaderMatch { + return new HeaderMatchImpl(headerName, false, { prefix }); + } + + /** + * The value of the header with the given name in the request must not start + * with the specified characters. + * + * @param headerName the name of the HTTP header to match against + * @param prefix The prefix to test against + */ + static valueDoesNotStartWith(headerName: string, prefix: string): HttpHeaderMatch { + return new HeaderMatchImpl(headerName, true, { prefix }); + } + + /** + * The value of the header with the given name in the request must end with + * the specified characters. + * + * @param headerName the name of the HTTP header to match against + * @param suffix The suffix to test against + */ + static valueEndsWith(headerName: string, suffix: string): HttpHeaderMatch { + return new HeaderMatchImpl(headerName, false, { suffix }); + } + + /** + * The value of the header with the given name in the request must not end + * with the specified characters. + * + * @param headerName the name of the HTTP header to match against + * @param suffix The suffix to test against + */ + static valueDoesNotEndWith(headerName: string, suffix: string): HttpHeaderMatch { + return new HeaderMatchImpl(headerName, true, { suffix }); + } + + /** + * The value of the header with the given name in the request must include + * the specified characters. + * + * @param headerName the name of the HTTP header to match against + * @param regex The regex to test against + */ + static valueMatchesRegex(headerName: string, regex: string): HttpHeaderMatch { + return new HeaderMatchImpl(headerName, false, { regex }); + } + + /** + * The value of the header with the given name in the request must not + * include the specified characters. + * + * @param headerName the name of the HTTP header to match against + * @param regex The regex to test against + */ + static valueDoesNotMatchRegex(headerName: string, regex: string): HttpHeaderMatch { + return new HeaderMatchImpl(headerName, true, { regex }); + } + + /** + * The value of the header with the given name in the request must be in a + * range of values. + * + * @param headerName the name of the HTTP header to match against + * @param start Match on values starting at and including this value + * @param end Match on values up to but not including this value + */ + static valuesIsInRange(headerName: string, start: number, end: number): HttpHeaderMatch { + return new HeaderMatchImpl(headerName, false, { + range: { + start, + end, + }, + }); + } + + /** + * The value of the header with the given name in the request must not be in + * a range of values. + * + * @param headerName the name of the HTTP header to match against + * @param start Match on values starting at and including this value + * @param end Match on values up to but not including this value + */ + static valuesIsNotInRange(headerName: string, start: number, end: number): HttpHeaderMatch { + return new HeaderMatchImpl(headerName, true, { + range: { + start, + end, + }, + }); + } + + /** + * Returns the header match configuration. + */ + abstract bind(scope: Construct): HttpHeaderMatchConfig; +} + +class HeaderMatchImpl extends HttpHeaderMatch { + constructor( + private readonly headerName: string, + private readonly invert: boolean, + private readonly matchProperty: CfnRoute.HeaderMatchMethodProperty, + ) { + super(); + } + + bind(_scope: Construct): HttpHeaderMatchConfig { + return { + httpRouteHeader: { + name: this.headerName, + invert: this.invert, + match: this.matchProperty, + }, + }; + } } /** @@ -43,10 +293,23 @@ export interface GrpcRouteMatch { readonly serviceName: string; } +/** + * Base options for all route specs. + */ +export interface RouteSpecOptionsBase { + /** + * The priority for the route. Routes are matched based on the specified + * value, where 0 is the highest priority. + * + * @default - no particular priority + */ + readonly priority?: number; +} + /** * Properties specific for HTTP Based Routes */ -export interface HttpRouteSpecOptions { +export interface HttpRouteSpecOptions extends RouteSpecOptionsBase { /** * The criterion for determining a request match for this Route * @@ -65,12 +328,87 @@ export interface HttpRouteSpecOptions { * @default - None */ readonly timeout?: HttpTimeout; + + /** + * The retry policy + * + * @default - no retry policy + */ + readonly retryPolicy?: HttpRetryPolicy; +} + +/** + * HTTP retry policy + */ +export interface HttpRetryPolicy { + /** + * Specify HTTP events on which to retry. You must specify at least one value + * for at least one types of retry events. + * + * @default - no retries for http events + */ + readonly httpRetryEvents?: HttpRetryEvent[]; + + /** + * The maximum number of retry attempts + */ + readonly retryAttempts: number; + + /** + * The timeout for each retry attempt + */ + readonly retryTimeout: Duration; + + /** + * TCP events on which to retry. The event occurs before any processing of a + * request has started and is encountered when the upstream is temporarily or + * permanently unavailable. You must specify at least one value for at least + * one types of retry events. + * + * @default - no retries for tcp events + */ + readonly tcpRetryEvents?: TcpRetryEvent[]; +} + +/** + * HTTP events on which to retry. + */ +export enum HttpRetryEvent { + /** + * HTTP status codes 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, and 511 + */ + SERVER_ERROR = 'server-error', + + /** + * HTTP status codes 502, 503, and 504 + */ + GATEWAY_ERROR = 'gateway-error', + + /** + * HTTP status code 409 + */ + CLIENT_ERROR = 'client-error', + + /** + * Retry on refused stream + */ + STREAM_ERROR = 'stream-error', +} + +/** + * TCP events on which you may retry + */ +export enum TcpRetryEvent { + /** + * A connection error + */ + CONNECTION_ERROR = 'connection-error', } /** * Properties specific for a TCP Based Routes */ -export interface TcpRouteSpecOptions { +export interface TcpRouteSpecOptions extends RouteSpecOptionsBase { /** * List of targets that traffic is routed to when a request matches the route */ @@ -87,7 +425,7 @@ export interface TcpRouteSpecOptions { /** * Properties specific for a GRPC Based Routes */ -export interface GrpcRouteSpecOptions { +export interface GrpcRouteSpecOptions extends RouteSpecOptionsBase { /** * The criterion for determining a request match for this Route */ @@ -104,6 +442,64 @@ export interface GrpcRouteSpecOptions { * List of targets that traffic is routed to when a request matches the route */ readonly weightedTargets: WeightedTarget[]; + + /** + * The retry policy + * + * @default - no retry policy + */ + readonly retryPolicy?: GrpcRetryPolicy; +} + +/** gRPC retry policy */ +export interface GrpcRetryPolicy extends HttpRetryPolicy { + /** + * gRPC events on which to retry. You must specify at least one value + * for at least one types of retry events. + * + * @default - no retries for gRPC events + */ + readonly grpcRetryEvents?: GrpcRetryEvent[]; +} + +/** + * gRPC events + */ +export enum GrpcRetryEvent { + /** + * Request was cancelled + * + * @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html + */ + CANCELLED = 'cancelled', + + /** + * The deadline was exceeded + * + * @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html + */ + DEADLINE_EXCEEDED = 'deadline-exceeded', + + /** + * Internal error + * + * @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html + */ + INTERNAL_ERROR = 'internal', + + /** + * A resource was exhausted + * + * @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html + */ + RESOURCE_EXHAUSTED = 'resource-exhausted', + + /** + * The service is unavailable + * + * @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html + */ + UNAVAILABLE = 'unavailable', } /** @@ -137,6 +533,14 @@ export interface RouteSpecConfig { * @default - no tcp spec */ readonly tcpRouteSpec?: CfnRoute.TcpRouteProperty; + + /** + * The priority for the route. Routes are matched based on the specified + * value, where 0 is the highest priority. + * + * @default - no particular priority + */ + readonly priority?: number; } /** @@ -180,25 +584,17 @@ export abstract class RouteSpec { } class HttpRouteSpec extends RouteSpec { - /** - * Type of route you are creating - */ + public readonly priority?: number; public readonly protocol: Protocol; - - /** - * The criteria for determining a request match - */ public readonly match?: HttpRouteMatch; - - /** - * The criteria for determining a timeout configuration - */ public readonly timeout?: HttpTimeout; + public readonly weightedTargets: WeightedTarget[]; + /** - * List of targets that traffic is routed to when a request matches the route + * The retry policy */ - public readonly weightedTargets: WeightedTarget[]; + public readonly retryPolicy?: HttpRetryPolicy; constructor(props: HttpRouteSpecOptions, protocol: Protocol) { super(); @@ -206,23 +602,45 @@ class HttpRouteSpec extends RouteSpec { this.match = props.match; this.weightedTargets = props.weightedTargets; this.timeout = props.timeout; + this.priority = props.priority; + + if (props.retryPolicy) { + const httpRetryEvents = props.retryPolicy.httpRetryEvents ?? []; + const tcpRetryEvents = props.retryPolicy.tcpRetryEvents ?? []; + + if (httpRetryEvents.length + tcpRetryEvents.length === 0) { + throw new Error('You must specify one value for at least one of `httpRetryEvents` or `tcpRetryEvents`'); + } + + this.retryPolicy = { + ...props.retryPolicy, + httpRetryEvents: httpRetryEvents.length > 0 ? httpRetryEvents : undefined, + tcpRetryEvents: tcpRetryEvents.length > 0 ? tcpRetryEvents : undefined, + }; + } } - public bind(_scope: Construct): RouteSpecConfig { + public bind(scope: Construct): RouteSpecConfig { const prefixPath = this.match ? this.match.prefixPath : '/'; if (prefixPath[0] != '/') { throw new Error(`Prefix Path must start with \'/\', got: ${prefixPath}`); } + const httpConfig: CfnRoute.HttpRouteProperty = { action: { weightedTargets: renderWeightedTargets(this.weightedTargets), }, match: { prefix: prefixPath, + headers: this.match?.headers?.map(header => header.bind(scope).httpRouteHeader), + method: this.match?.method, + scheme: this.match?.protocol, }, timeout: renderTimeout(this.timeout), + retryPolicy: this.retryPolicy ? renderHttpRetryPolicy(this.retryPolicy) : undefined, }; return { + priority: this.priority, httpRouteSpec: this.protocol === Protocol.HTTP ? httpConfig : undefined, http2RouteSpec: this.protocol === Protocol.HTTP2 ? httpConfig : undefined, }; @@ -230,6 +648,11 @@ class HttpRouteSpec extends RouteSpec { } class TcpRouteSpec extends RouteSpec { + /** + * The priority for the route. + */ + public readonly priority?: number; + /* * List of targets that traffic is routed to when a request matches the route */ @@ -244,10 +667,12 @@ class TcpRouteSpec extends RouteSpec { super(); this.weightedTargets = props.weightedTargets; this.timeout = props.timeout; + this.priority = props.priority; } public bind(_scope: Construct): RouteSpecConfig { return { + priority: this.priority, tcpRouteSpec: { action: { weightedTargets: renderWeightedTargets(this.weightedTargets), @@ -259,19 +684,48 @@ class TcpRouteSpec extends RouteSpec { } class GrpcRouteSpec extends RouteSpec { + /** + * The priority for the route. + */ + public readonly priority?: number; + public readonly weightedTargets: WeightedTarget[]; public readonly match: GrpcRouteMatch; public readonly timeout?: GrpcTimeout; + /** + * The retry policy. + */ + public readonly retryPolicy?: GrpcRetryPolicy; + constructor(props: GrpcRouteSpecOptions) { super(); this.weightedTargets = props.weightedTargets; this.match = props.match; this.timeout = props.timeout; + this.priority = props.priority; + + if (props.retryPolicy) { + const grpcRetryEvents = props.retryPolicy.grpcRetryEvents ?? []; + const httpRetryEvents = props.retryPolicy.httpRetryEvents ?? []; + const tcpRetryEvents = props.retryPolicy.tcpRetryEvents ?? []; + + if (grpcRetryEvents.length + httpRetryEvents.length + tcpRetryEvents.length === 0) { + throw new Error('You must specify one value for at least one of `grpcRetryEvents`, `httpRetryEvents` or `tcpRetryEvents`'); + } + + this.retryPolicy = { + ...props.retryPolicy, + grpcRetryEvents: grpcRetryEvents.length > 0 ? grpcRetryEvents : undefined, + httpRetryEvents: httpRetryEvents.length > 0 ? httpRetryEvents : undefined, + tcpRetryEvents: tcpRetryEvents.length > 0 ? tcpRetryEvents : undefined, + }; + } } public bind(_scope: Construct): RouteSpecConfig { return { + priority: this.priority, grpcRouteSpec: { action: { weightedTargets: renderWeightedTargets(this.weightedTargets), @@ -280,6 +734,7 @@ class GrpcRouteSpec extends RouteSpec { serviceName: this.match.serviceName, }, timeout: renderTimeout(this.timeout), + retryPolicy: this.retryPolicy ? renderGrpcRetryPolicy(this.retryPolicy) : undefined, }, }; } @@ -320,3 +775,22 @@ function renderTimeout(timeout?: HttpTimeout): CfnRoute.HttpTimeoutProperty | un } : undefined; } + +function renderHttpRetryPolicy(retryPolicy: HttpRetryPolicy): CfnRoute.HttpRetryPolicyProperty { + return { + maxRetries: retryPolicy.retryAttempts, + perRetryTimeout: { + unit: 'ms', + value: retryPolicy.retryTimeout.toMilliseconds(), + }, + httpRetryEvents: retryPolicy.httpRetryEvents, + tcpRetryEvents: retryPolicy.tcpRetryEvents, + }; +} + +function renderGrpcRetryPolicy(retryPolicy: GrpcRetryPolicy): CfnRoute.GrpcRetryPolicyProperty { + return { + ...renderHttpRetryPolicy(retryPolicy), + grpcRetryEvents: retryPolicy.grpcRetryEvents, + }; +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/route.ts b/packages/@aws-cdk/aws-appmesh/lib/route.ts index 7b9bd2aeb94d2..7800e3e08e53f 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route.ts @@ -126,6 +126,7 @@ export class Route extends cdk.Resource implements IRoute { httpRoute: spec.httpRouteSpec, http2Route: spec.http2RouteSpec, grpcRoute: spec.grpcRouteSpec, + priority: spec.priority, }, }); diff --git a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts index 01a103cb2dee9..98caa8dcb8025 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts @@ -1,6 +1,8 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVirtualGateway, CfnVirtualNode } from './appmesh.generated'; +import { ClientPolicy } from './client-policy'; +import { IVirtualService } from './virtual-service'; /** * Represents timeouts for HTTP protocols. @@ -191,3 +193,80 @@ class FileAccessLog extends AccessLog { } } +/** + * Represents the properties needed to define backend defaults + */ +export interface BackendDefaults { + /** + * Client policy for backend defaults + * + * @default none + */ + readonly clientPolicy?: ClientPolicy; +} + +/** + * Represents the properties needed to define a Virtual Service backend + */ +export interface VirtualServiceBackendOptions { + + /** + * Client policy for the backend + * + * @default none + */ + readonly clientPolicy?: ClientPolicy; +} + +/** + * Properties for a backend + */ +export interface BackendConfig { + /** + * Config for a Virtual Service backend + */ + readonly virtualServiceBackend: CfnVirtualNode.BackendProperty; +} + + +/** + * Contains static factory methods to create backends + */ +export abstract class Backend { + /** + * Construct a Virtual Service backend + */ + public static virtualService(virtualService: IVirtualService, props: VirtualServiceBackendOptions = {}): Backend { + return new VirtualServiceBackend(virtualService, props.clientPolicy); + } + + /** + * Return backend config + */ + public abstract bind(_scope: Construct): BackendConfig; +} + +/** + * Represents the properties needed to define a Virtual Service backend + */ +class VirtualServiceBackend extends Backend { + + constructor (private readonly virtualService: IVirtualService, + private readonly clientPolicy: ClientPolicy | undefined) { + super(); + } + + /** + * Return config for a Virtual Service backend + */ + public bind(_scope: Construct): BackendConfig { + return { + virtualServiceBackend: { + virtualService: { + virtualServiceName: this.virtualService.virtualServiceName, + clientPolicy: this.clientPolicy?.bind(_scope).clientPolicy, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts index 6c544f0b657a0..3b1df58a9360c 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts @@ -1,10 +1,9 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVirtualGateway } from './appmesh.generated'; -import { ClientPolicy } from './client-policy'; import { GatewayRoute, GatewayRouteBaseProps } from './gateway-route'; import { IMesh, Mesh } from './mesh'; -import { AccessLog } from './shared-interfaces'; +import { AccessLog, BackendDefaults } from './shared-interfaces'; import { VirtualGatewayListener, VirtualGatewayListenerConfig } from './virtual-gateway-listener'; /** @@ -66,7 +65,7 @@ export interface VirtualGatewayBaseProps { * * @default - No Config */ - readonly backendsDefaultClientPolicy?: ClientPolicy; + readonly backendDefaults?: BackendDefaults; } /** @@ -180,7 +179,11 @@ export class VirtualGateway extends VirtualGatewayBase { meshName: this.mesh.meshName, spec: { listeners: this.listeners.map(listener => listener.listener), - backendDefaults: props.backendsDefaultClientPolicy?.bind(this), + backendDefaults: props.backendDefaults !== undefined + ? { + clientPolicy: props.backendDefaults?.clientPolicy?.bind(this).clientPolicy, + } + : undefined, logging: accessLogging !== undefined ? { accessLog: accessLogging.virtualGatewayAccessLog, } : undefined, diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts index 2cf56c74631a2..60ca92bb142ca 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts @@ -1,12 +1,10 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVirtualNode } from './appmesh.generated'; -import { ClientPolicy } from './client-policy'; import { IMesh, Mesh } from './mesh'; import { ServiceDiscovery } from './service-discovery'; -import { AccessLog } from './shared-interfaces'; +import { AccessLog, BackendDefaults, Backend } from './shared-interfaces'; import { VirtualNodeListener, VirtualNodeListenerConfig } from './virtual-node-listener'; -import { IVirtualService } from './virtual-service'; /** * Interface which all VirtualNode based classes must implement @@ -61,7 +59,7 @@ export interface VirtualNodeBaseProps { * * @default - No backends */ - readonly backends?: IVirtualService[]; + readonly backends?: Backend[]; /** * Initial listener for the virtual node @@ -82,7 +80,7 @@ export interface VirtualNodeBaseProps { * * @default - No Config */ - readonly backendsDefaultClientPolicy?: ClientPolicy; + readonly backendDefaults?: BackendDefaults; } /** @@ -185,7 +183,11 @@ export class VirtualNode extends VirtualNodeBase { spec: { backends: cdk.Lazy.anyValue({ produce: () => this.backends }, { omitEmptyArray: true }), listeners: cdk.Lazy.anyValue({ produce: () => this.listeners.map(listener => listener.listener) }, { omitEmptyArray: true }), - backendDefaults: props.backendsDefaultClientPolicy?.bind(this), + backendDefaults: props.backendDefaults !== undefined + ? { + clientPolicy: props.backendDefaults?.clientPolicy?.bind(this).clientPolicy, + } + : undefined, serviceDiscovery: { dns: serviceDiscovery?.dns, awsCloudMap: serviceDiscovery?.cloudmap, @@ -214,13 +216,8 @@ export class VirtualNode extends VirtualNodeBase { /** * Add a Virtual Services that this node is expected to send outbound traffic to */ - public addBackend(virtualService: IVirtualService) { - this.backends.push({ - virtualService: { - virtualServiceName: virtualService.virtualServiceName, - clientPolicy: virtualService.clientPolicy?.bind(this).clientPolicy, - }, - }); + public addBackend(backend: Backend) { + this.backends.push(backend.bind(this).virtualServiceBackend); } } diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts index 5685b8b08c1f8..d41b47d554178 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts @@ -1,7 +1,6 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVirtualService } from './appmesh.generated'; -import { ClientPolicy } from './client-policy'; import { IMesh, Mesh } from './mesh'; import { IVirtualNode } from './virtual-node'; import { IVirtualRouter } from './virtual-router'; @@ -28,11 +27,6 @@ export interface IVirtualService extends cdk.IResource { * The Mesh which the VirtualService belongs to */ readonly mesh: IMesh; - - /** - * Client policy for this Virtual Service - */ - readonly clientPolicy?: ClientPolicy; } /** @@ -50,13 +44,6 @@ export interface VirtualServiceProps { */ readonly virtualServiceName?: string; - /** - * Client policy for this Virtual Service - * - * @default - none - */ - readonly clientPolicy?: ClientPolicy; - /** * The VirtualNode or VirtualRouter which the VirtualService uses as its provider */ @@ -90,7 +77,6 @@ export class VirtualService extends cdk.Resource implements IVirtualService { return new class extends cdk.Resource implements IVirtualService { readonly virtualServiceName = attrs.virtualServiceName; readonly mesh = attrs.mesh; - readonly clientPolicy = attrs.clientPolicy; readonly virtualServiceArn = cdk.Stack.of(this).formatArn({ service: 'appmesh', resource: `mesh/${attrs.mesh.meshName}/virtualService`, @@ -114,14 +100,11 @@ export class VirtualService extends cdk.Resource implements IVirtualService { */ public readonly mesh: IMesh; - public readonly clientPolicy?: ClientPolicy; - constructor(scope: Construct, id: string, props: VirtualServiceProps) { super(scope, id, { physicalName: props.virtualServiceName || cdk.Lazy.string({ produce: () => cdk.Names.uniqueId(this) }), }); - this.clientPolicy = props.clientPolicy; const providerConfig = props.virtualServiceProvider.bind(this); this.mesh = providerConfig.mesh; @@ -160,13 +143,6 @@ export interface VirtualServiceAttributes { * The Mesh which the VirtualService belongs to */ readonly mesh: IMesh; - - /** - * Client policy for this Virtual Service - * - * @default - none - */ - readonly clientPolicy?: ClientPolicy; } /** diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json index 5f4a9ca206725..c3139e2b75582 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -497,7 +497,6 @@ "MeshName" ] }, - "RouteName": "route-1", "Spec": { "HttpRoute": { "Action": { @@ -533,7 +532,8 @@ "meshrouter81B8087E", "VirtualRouterName" ] - } + }, + "RouteName": "route-1" } }, "meshrouterroute2486D9DEF": { @@ -545,7 +545,6 @@ "MeshName" ] }, - "RouteName": "route-2", "Spec": { "HttpRoute": { "Action": { @@ -581,7 +580,8 @@ "meshrouter81B8087E", "VirtualRouterName" ] - } + }, + "RouteName": "route-2" } }, "meshrouterroute3BD0FA22F": { @@ -593,7 +593,6 @@ "MeshName" ] }, - "RouteName": "route-3", "Spec": { "TcpRoute": { "Action": { @@ -622,7 +621,270 @@ "meshrouter81B8087E", "VirtualRouterName" ] - } + }, + "RouteName": "route-3" + } + }, + "meshrouterroutematchingACC12F04": { + "Type": "AWS::AppMesh::Route", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Http2Route": { + "Action": { + "WeightedTargets": [ + { + "VirtualNode": { + "Fn::GetAtt": [ + "meshnode3D2A19CF2", + "VirtualNodeName" + ] + }, + "Weight": 1 + } + ] + }, + "Match": { + "Headers": [ + { + "Invert": false, + "Match": { + "Exact": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Prefix": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Suffix": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Regex": "application/.*" + }, + "Name": "Content-Type" + }, + { + "Invert": false, + "Match": { + "Range": { + "End": 5, + "Start": 1 + } + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Exact": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Prefix": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Suffix": "application/json" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Regex": "application/.*" + }, + "Name": "Content-Type" + }, + { + "Invert": true, + "Match": { + "Range": { + "End": 5, + "Start": 1 + } + }, + "Name": "Content-Type" + } + ], + "Method": "POST", + "Prefix": "/", + "Scheme": "https" + } + } + }, + "VirtualRouterName": { + "Fn::GetAtt": [ + "meshrouter81B8087E", + "VirtualRouterName" + ] + }, + "RouteName": "route-matching" + } + }, + "meshrouterroutehttp2retryCC41345F": { + "Type": "AWS::AppMesh::Route", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Http2Route": { + "Action": { + "WeightedTargets": [ + { + "VirtualNode": { + "Fn::GetAtt": [ + "meshnode3D2A19CF2", + "VirtualNodeName" + ] + }, + "Weight": 1 + } + ] + }, + "Match": { + "Prefix": "/" + }, + "RetryPolicy": { + "HttpRetryEvents": [ + "client-error" + ], + "MaxRetries": 5, + "PerRetryTimeout": { + "Unit": "ms", + "Value": 1000 + }, + "TcpRetryEvents": [ + "connection-error" + ] + } + } + }, + "VirtualRouterName": { + "Fn::GetAtt": [ + "meshrouter81B8087E", + "VirtualRouterName" + ] + }, + "RouteName": "route-http2-retry" + } + }, + "meshrouterroute53F46B0FE": { + "Type": "AWS::AppMesh::Route", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Http2Route": { + "Action": { + "WeightedTargets": [ + { + "VirtualNode": { + "Fn::GetAtt": [ + "meshnode2092BA426", + "VirtualNodeName" + ] + }, + "Weight": 1 + } + ] + }, + "Match": { + "Prefix": "/" + } + }, + "Priority": 10 + }, + "VirtualRouterName": { + "Fn::GetAtt": [ + "meshrouter81B8087E", + "VirtualRouterName" + ] + }, + "RouteName": "route-5" + } + }, + "meshrouterroutegrpcretry9BEB798A": { + "Type": "AWS::AppMesh::Route", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "GrpcRoute": { + "Action": { + "WeightedTargets": [ + { + "VirtualNode": { + "Fn::GetAtt": [ + "meshnode3D2A19CF2", + "VirtualNodeName" + ] + }, + "Weight": 1 + } + ] + }, + "Match": { + "ServiceName": "servicename" + }, + "RetryPolicy": { + "GrpcRetryEvents": [ + "deadline-exceeded" + ], + "HttpRetryEvents": [ + "client-error" + ], + "MaxRetries": 5, + "PerRetryTimeout": { + "Unit": "ms", + "Value": 1000 + }, + "TcpRetryEvents": [ + "connection-error" + ] + } + } + }, + "VirtualRouterName": { + "Fn::GetAtt": [ + "meshrouter81B8087E", + "VirtualRouterName" + ] + }, + "RouteName": "route-grpc-retry" } }, "meshnode726C787D": { @@ -832,7 +1094,6 @@ "meshgateway1gateway1routehttpE8D6F433": { "Type": "AWS::AppMesh::GatewayRoute", "Properties": { - "GatewayRouteName": "meshstackmeshgateway1gateway1routehttpBA921D42", "MeshName": { "Fn::GetAtt": [ "meshACDFE68E", @@ -863,13 +1124,13 @@ "meshgateway1B02387E8", "VirtualGatewayName" ] - } + }, + "GatewayRouteName": "meshstackmeshgateway1gateway1routehttpBA921D42" } }, "meshgateway1gateway1routehttp2FD69C306": { "Type": "AWS::AppMesh::GatewayRoute", "Properties": { - "GatewayRouteName": "meshstackmeshgateway1gateway1routehttp255781963", "MeshName": { "Fn::GetAtt": [ "meshACDFE68E", @@ -900,13 +1161,13 @@ "meshgateway1B02387E8", "VirtualGatewayName" ] - } + }, + "GatewayRouteName": "meshstackmeshgateway1gateway1routehttp255781963" } }, "meshgateway1gateway1routegrpc76486062": { "Type": "AWS::AppMesh::GatewayRoute", "Properties": { - "GatewayRouteName": "meshstackmeshgateway1gateway1routegrpcCD4D891D", "MeshName": { "Fn::GetAtt": [ "meshACDFE68E", @@ -942,7 +1203,8 @@ "meshgateway1B02387E8", "VirtualGatewayName" ] - } + }, + "GatewayRouteName": "meshstackmeshgateway1gateway1routegrpcCD4D891D" } }, "service6D174F83": { diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index 90e54586f7f51..68709def26f95 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -36,16 +36,15 @@ const node = mesh.addVirtualNode('node', { path: '/check-path', }, })], - backends: [ - virtualService, - ], + backends: [appmesh.Backend.virtualService(virtualService)], }); -node.addBackend(new appmesh.VirtualService(stack, 'service-2', { - virtualServiceName: 'service2.domain.local', - virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), -}), -); +node.addBackend(appmesh.Backend.virtualService( + new appmesh.VirtualService(stack, 'service-2', { + virtualServiceName: 'service2.domain.local', + virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + }), +)); router.addRoute('route-1', { routeSpec: appmesh.RouteSpec.http({ @@ -78,15 +77,17 @@ const node2 = mesh.addVirtualNode('node2', { unhealthyThreshold: 2, }, })], - backendsDefaultClientPolicy: appmesh.ClientPolicy.fileTrust({ - certificateChain: 'path/to/cert', - }), - backends: [ + backendDefaults: { + clientPolicy: appmesh.ClientPolicy.fileTrust({ + certificateChain: 'path/to/cert', + }), + }, + backends: [appmesh.Backend.virtualService( new appmesh.VirtualService(stack, 'service-3', { virtualServiceName: 'service3.domain.local', virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }), - ], + )], }); const node3 = mesh.addVirtualNode('node3', { @@ -102,9 +103,11 @@ const node3 = mesh.addVirtualNode('node3', { unhealthyThreshold: 2, }, })], - backendsDefaultClientPolicy: appmesh.ClientPolicy.fileTrust({ - certificateChain: 'path-to-certificate', - }), + backendDefaults: { + clientPolicy: appmesh.ClientPolicy.fileTrust({ + certificateChain: 'path-to-certificate', + }), + }, accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), }); @@ -140,6 +143,62 @@ router.addRoute('route-3', { }), }); +router.addRoute('route-matching', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode: node3 }], + match: { + prefixPath: '/', + method: appmesh.HttpRouteMatchMethod.POST, + protocol: appmesh.HttpRouteProtocol.HTTPS, + headers: [ + appmesh.HttpHeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HttpHeaderMatch.valueStartsWith('Content-Type', 'application/json'), + appmesh.HttpHeaderMatch.valueEndsWith('Content-Type', 'application/json'), + appmesh.HttpHeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HttpHeaderMatch.valuesIsInRange('Content-Type', 1, 5), + appmesh.HttpHeaderMatch.valueIsNot('Content-Type', 'application/json'), + appmesh.HttpHeaderMatch.valueDoesNotStartWith('Content-Type', 'application/json'), + appmesh.HttpHeaderMatch.valueDoesNotEndWith('Content-Type', 'application/json'), + appmesh.HttpHeaderMatch.valueDoesNotMatchRegex('Content-Type', 'application/.*'), + appmesh.HttpHeaderMatch.valuesIsNotInRange('Content-Type', 1, 5), + ], + }, + }), +}); + +router.addRoute('route-http2-retry', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode: node3 }], + retryPolicy: { + httpRetryEvents: [appmesh.HttpRetryEvent.CLIENT_ERROR], + tcpRetryEvents: [appmesh.TcpRetryEvent.CONNECTION_ERROR], + retryAttempts: 5, + retryTimeout: cdk.Duration.seconds(1), + }, + }), +}); + +router.addRoute('route-5', { + routeSpec: appmesh.RouteSpec.http2({ + priority: 10, + weightedTargets: [{ virtualNode: node2 }], + }), +}); + +router.addRoute('route-grpc-retry', { + routeSpec: appmesh.RouteSpec.grpc({ + weightedTargets: [{ virtualNode: node3 }], + match: { serviceName: 'servicename' }, + retryPolicy: { + grpcRetryEvents: [appmesh.GrpcRetryEvent.DEADLINE_EXCEEDED], + httpRetryEvents: [appmesh.HttpRetryEvent.CLIENT_ERROR], + tcpRetryEvents: [appmesh.TcpRetryEvent.CONNECTION_ERROR], + retryAttempts: 5, + retryTimeout: cdk.Duration.seconds(1), + }, + }), +}); + const gateway = mesh.addVirtualGateway('gateway1', { accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), virtualGatewayName: 'gateway1', diff --git a/packages/@aws-cdk/aws-appmesh/test/test.health-check.ts b/packages/@aws-cdk/aws-appmesh/test/test.health-check.ts index 7eec2b6d450b9..1ba7dc425da07 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.health-check.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.health-check.ts @@ -66,8 +66,6 @@ export = { // THEN test.doesNotThrow(() => toThrow(min)); test.doesNotThrow(() => toThrow(max)); - // falsy, falls back to portMapping.port - // test.throws(() => toThrow(min - 1), /below the minimum threshold/); test.throws(() => toThrow(max + 1), /above the maximum threshold/); test.done(); diff --git a/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts index ce50c1402a7c3..5c9c1cea7a9a1 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts @@ -270,9 +270,7 @@ export = { listeners: [appmesh.VirtualNodeListener.http({ port: 8080, })], - backends: [ - service1, - ], + backends: [appmesh.Backend.virtualService(service1)], }); // THEN diff --git a/packages/@aws-cdk/aws-appmesh/test/test.route.ts b/packages/@aws-cdk/aws-appmesh/test/test.route.ts index cb5c92e6464cf..b3c1ae674a6f4 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.route.ts @@ -1,4 +1,4 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert'; +import { ABSENT, expect, haveResourceLike } from '@aws-cdk/assert'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; @@ -282,6 +282,570 @@ export = { })); test.done(); }, + + 'should allow http retries'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + router.addRoute('test-http-route', { + routeSpec: appmesh.RouteSpec.http({ + weightedTargets: [{ virtualNode }], + retryPolicy: { + httpRetryEvents: [appmesh.HttpRetryEvent.CLIENT_ERROR], + tcpRetryEvents: [appmesh.TcpRetryEvent.CONNECTION_ERROR], + retryAttempts: 5, + retryTimeout: cdk.Duration.seconds(10), + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + HttpRoute: { + RetryPolicy: { + HttpRetryEvents: ['client-error'], + TcpRetryEvents: ['connection-error'], + MaxRetries: 5, + PerRetryTimeout: { + Unit: 'ms', + Value: 10000, + }, + }, + }, + }, + })); + + test.done(); + }, + + 'http retry events are ABSENT when specified as an empty array'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + router.addRoute('test-http-route', { + routeSpec: appmesh.RouteSpec.http({ + weightedTargets: [{ virtualNode }], + retryPolicy: { + httpRetryEvents: [], + tcpRetryEvents: [appmesh.TcpRetryEvent.CONNECTION_ERROR], + retryAttempts: 5, + retryTimeout: cdk.Duration.seconds(10), + }, + }), + }); + + router.addRoute('test-http-route2', { + routeSpec: appmesh.RouteSpec.http({ + weightedTargets: [{ virtualNode }], + retryPolicy: { + httpRetryEvents: [appmesh.HttpRetryEvent.CLIENT_ERROR], + tcpRetryEvents: [], + retryAttempts: 5, + retryTimeout: cdk.Duration.seconds(10), + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + HttpRoute: { + RetryPolicy: { + HttpRetryEvents: ABSENT, + TcpRetryEvents: ['connection-error'], + }, + }, + }, + })); + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + HttpRoute: { + RetryPolicy: { + HttpRetryEvents: ['client-error'], + TcpRetryEvents: ABSENT, + }, + }, + }, + })); + + test.done(); + }, + + 'errors when http retry policy has no events'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + test.throws(() => { + router.addRoute('test-http-route', { + routeSpec: appmesh.RouteSpec.http({ + weightedTargets: [{ virtualNode }], + retryPolicy: { + retryAttempts: 5, + retryTimeout: cdk.Duration.seconds(10), + }, + }), + }); + }, /specify one value for at least/i); + + test.done(); + }, + + 'should allow grpc retries'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + router.addRoute('test-grpc-route', { + routeSpec: appmesh.RouteSpec.grpc({ + weightedTargets: [{ virtualNode }], + match: { serviceName: 'servicename' }, + retryPolicy: { + grpcRetryEvents: [appmesh.GrpcRetryEvent.DEADLINE_EXCEEDED], + httpRetryEvents: [appmesh.HttpRetryEvent.CLIENT_ERROR], + tcpRetryEvents: [appmesh.TcpRetryEvent.CONNECTION_ERROR], + retryAttempts: 5, + retryTimeout: cdk.Duration.seconds(10), + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + GrpcRoute: { + RetryPolicy: { + GrpcRetryEvents: ['deadline-exceeded'], + HttpRetryEvents: ['client-error'], + TcpRetryEvents: ['connection-error'], + MaxRetries: 5, + PerRetryTimeout: { + Unit: 'ms', + Value: 10000, + }, + }, + }, + }, + })); + + test.done(); + }, + + 'grpc retry events are ABSENT when specified as an empty array'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + router.addRoute('test-grpc-route', { + routeSpec: appmesh.RouteSpec.grpc({ + weightedTargets: [{ virtualNode }], + match: { serviceName: 'example' }, + retryPolicy: { + grpcRetryEvents: [], + httpRetryEvents: [], + tcpRetryEvents: [appmesh.TcpRetryEvent.CONNECTION_ERROR], + retryAttempts: 5, + retryTimeout: cdk.Duration.seconds(10), + }, + }), + }); + + router.addRoute('test-grpc-route2', { + routeSpec: appmesh.RouteSpec.grpc({ + weightedTargets: [{ virtualNode }], + match: { serviceName: 'example' }, + retryPolicy: { + grpcRetryEvents: [appmesh.GrpcRetryEvent.CANCELLED], + httpRetryEvents: [], + tcpRetryEvents: [], + retryAttempts: 5, + retryTimeout: cdk.Duration.seconds(10), + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + GrpcRoute: { + RetryPolicy: { + GrpcRetryEvents: ABSENT, + HttpRetryEvents: ABSENT, + TcpRetryEvents: ['connection-error'], + }, + }, + }, + })); + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + GrpcRoute: { + RetryPolicy: { + GrpcRetryEvents: ['cancelled'], + HttpRetryEvents: ABSENT, + TcpRetryEvents: ABSENT, + }, + }, + }, + })); + + test.done(); + }, + + 'errors when grpc retry policy has no events'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + test.throws(() => { + router.addRoute('test-grpc-route', { + routeSpec: appmesh.RouteSpec.grpc({ + weightedTargets: [{ virtualNode }], + match: { serviceName: 'servicename' }, + retryPolicy: { + retryAttempts: 5, + retryTimeout: cdk.Duration.seconds(10), + }, + }), + }); + }, /specify one value for at least/i); + + test.done(); + }, + }, + + 'should match routes based on headers'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + router.addRoute('route', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode }], + match: { + prefixPath: '/', + headers: [ + appmesh.HttpHeaderMatch.valueIs('Content-Type', 'application/json'), + appmesh.HttpHeaderMatch.valueIsNot('Content-Type', 'text/html'), + appmesh.HttpHeaderMatch.valueStartsWith('Content-Type', 'application/'), + appmesh.HttpHeaderMatch.valueDoesNotStartWith('Content-Type', 'text/'), + appmesh.HttpHeaderMatch.valueEndsWith('Content-Type', '/json'), + appmesh.HttpHeaderMatch.valueDoesNotEndWith('Content-Type', '/json+foobar'), + appmesh.HttpHeaderMatch.valueMatchesRegex('Content-Type', 'application/.*'), + appmesh.HttpHeaderMatch.valueDoesNotMatchRegex('Content-Type', 'text/.*'), + appmesh.HttpHeaderMatch.valuesIsInRange('Max-Forward', 1, 5), + appmesh.HttpHeaderMatch.valuesIsNotInRange('Max-Forward', 1, 5), + ], + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Http2Route: { + Match: { + Prefix: '/', + Headers: [ + { + Invert: false, + Match: { Exact: 'application/json' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Exact: 'text/html' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Prefix: 'application/' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Prefix: 'text/' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Suffix: '/json' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Suffix: '/json+foobar' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { Regex: 'application/.*' }, + Name: 'Content-Type', + }, + { + Invert: true, + Match: { Regex: 'text/.*' }, + Name: 'Content-Type', + }, + { + Invert: false, + Match: { + Range: { + End: 5, + Start: 1, + }, + }, + Name: 'Max-Forward', + }, + { + Invert: true, + Match: { + Range: { + End: 5, + Start: 1, + }, + }, + Name: 'Max-Forward', + }, + ], + }, + }, + }, + })); + + test.done(); + }, + + 'should match routes based on method'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + router.addRoute('route', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode }], + match: { + prefixPath: '/', + method: appmesh.HttpRouteMatchMethod.GET, + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Http2Route: { + Match: { + Prefix: '/', + Method: 'GET', + }, + }, + }, + })); + + test.done(); + }, + + 'should match routes based on scheme'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + router.addRoute('route', { + routeSpec: appmesh.RouteSpec.http2({ + weightedTargets: [{ virtualNode }], + match: { + prefixPath: '/', + protocol: appmesh.HttpRouteProtocol.HTTP, + }, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Http2Route: { + Match: { + Prefix: '/', + Scheme: 'http', + }, + }, + }, + })); + + test.done(); + }, + + 'should allow route priority'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const router = new appmesh.VirtualRouter(stack, 'router', { + mesh, + }); + + const virtualNode = mesh.addVirtualNode('test-node', { + serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), + listeners: [appmesh.VirtualNodeListener.http()], + }); + + // WHEN + router.addRoute('http2', { + routeSpec: appmesh.RouteSpec.http2({ + priority: 0, + weightedTargets: [{ virtualNode }], + }), + }); + router.addRoute('http', { + routeSpec: appmesh.RouteSpec.http({ + priority: 10, + weightedTargets: [{ virtualNode }], + }), + }); + router.addRoute('grpc', { + routeSpec: appmesh.RouteSpec.grpc({ + priority: 20, + weightedTargets: [{ virtualNode }], + match: { + serviceName: 'test', + }, + }), + }); + router.addRoute('tcp', { + routeSpec: appmesh.RouteSpec.tcp({ + priority: 30, + weightedTargets: [{ virtualNode }], + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Priority: 0, + Http2Route: {}, + }, + })); + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Priority: 10, + HttpRoute: {}, + }, + })); + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Priority: 20, + GrpcRoute: {}, + }, + })); + expect(stack).to(haveResourceLike('AWS::AppMesh::Route', { + Spec: { + Priority: 30, + TcpRoute: {}, + }, + })); + + test.done(); }, 'Can import Routes using an ARN'(test: Test) { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts index b9d3ed70cae43..25b7974983f2a 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts @@ -392,9 +392,11 @@ export = { new appmesh.VirtualGateway(stack, 'virtual-gateway', { virtualGatewayName: 'virtual-gateway', mesh: mesh, - backendsDefaultClientPolicy: appmesh.ClientPolicy.fileTrust({ - certificateChain: 'path-to-certificate', - }), + backendDefaults: { + clientPolicy: appmesh.ClientPolicy.fileTrust({ + certificateChain: 'path-to-certificate', + }), + }, }); // THEN diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts index 4337973230854..c09bdef5badbd 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts @@ -29,10 +29,10 @@ export = { const node = new appmesh.VirtualNode(stack, 'test-node', { mesh, serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), - backends: [service1], + backends: [appmesh.Backend.virtualService(service1)], }); - node.addBackend(service2); + node.addBackend(appmesh.Backend.virtualService(service2)); // THEN expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualNode', { @@ -272,10 +272,12 @@ export = { new appmesh.VirtualNode(stack, 'test-node', { mesh, serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), - backendsDefaultClientPolicy: appmesh.ClientPolicy.acmTrust({ - certificateAuthorities: [acmpca.CertificateAuthority.fromCertificateAuthorityArn(stack, 'certificate', certificateAuthorityArn)], - ports: [8080, 8081], - }), + backendDefaults: { + clientPolicy: appmesh.ClientPolicy.acmTrust({ + certificateAuthorities: [acmpca.CertificateAuthority.fromCertificateAuthorityArn(stack, 'certificate', certificateAuthorityArn)], + ports: [8080, 8081], + }), + }, }); // THEN @@ -320,13 +322,14 @@ export = { const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), + }); + + node.addBackend(appmesh.Backend.virtualService(service1, { clientPolicy: appmesh.ClientPolicy.fileTrust({ certificateChain: 'path-to-certificate', ports: [8080, 8081], }), - }); - - node.addBackend(service1); + })); // THEN expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualNode', { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts index 2732adb4cba17..fef86e6bd7e7a 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts @@ -109,7 +109,7 @@ export = { listeners: [appmesh.VirtualNodeListener.http({ port: 8080, })], - backends: [service1], + backends: [appmesh.Backend.virtualService(service1)], }); router.addRoute('route-1', { @@ -182,27 +182,21 @@ export = { listeners: [appmesh.VirtualNodeListener.http({ port: 8080, })], - backends: [ - service1, - ], + backends: [appmesh.Backend.virtualService(service1)], }); const node2 = mesh.addVirtualNode('test-node2', { serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), listeners: [appmesh.VirtualNodeListener.http({ port: 8080, })], - backends: [ - service2, - ], + backends: [appmesh.Backend.virtualService(service2)], }); const node3 = mesh.addVirtualNode('test-node3', { serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), listeners: [appmesh.VirtualNodeListener.http({ port: 8080, })], - backends: [ - service1, - ], + backends: [appmesh.Backend.virtualService(service1)], }); router.addRoute('route-1', { @@ -340,9 +334,7 @@ export = { listeners: [appmesh.VirtualNodeListener.http({ port: 8080, })], - backends: [ - service1, - ], + backends: [appmesh.Backend.virtualService(service1)], }); router.addRoute('route-tcp-1', { diff --git a/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts b/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts index 01d8d715d8153..ff96848cf048e 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts @@ -48,6 +48,25 @@ export interface BasicStepScalingPolicyProps { * @default No minimum scaling effect */ readonly minAdjustmentMagnitude?: number; + + /** + * How many evaluation periods of the metric to wait before triggering a scaling action + * + * Raising this value can be used to smooth out the metric, at the expense + * of slower response times. + * + * @default 1 + */ + readonly evaluationPeriods?: number; + + /** + * Aggregation to apply to all data points over the evaluation periods + * + * Only has meaning if `evaluationPeriods != 1`. + * + * @default - The statistic from the metric if applicable (MIN, MAX, AVERAGE), otherwise AVERAGE. + */ + readonly metricAggregationType?: MetricAggregationType; } export interface StepScalingPolicyProps extends BasicStepScalingPolicyProps { @@ -89,7 +108,7 @@ export class StepScalingPolicy extends Construct { this.lowerAction = new StepScalingAction(this, 'LowerPolicy', { adjustmentType: props.adjustmentType, cooldown: props.cooldown, - metricAggregationType: aggregationTypeFromMetric(props.metric), + metricAggregationType: props.metricAggregationType ?? aggregationTypeFromMetric(props.metric), minAdjustmentMagnitude: props.minAdjustmentMagnitude, autoScalingGroup: props.autoScalingGroup, }); @@ -107,7 +126,7 @@ export class StepScalingPolicy extends Construct { metric: props.metric, alarmDescription: 'Lower threshold scaling alarm', comparisonOperator: cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD, - evaluationPeriods: 1, + evaluationPeriods: props.evaluationPeriods ?? 1, threshold, }); this.lowerAlarm.addAlarmAction(new StepScalingAlarmAction(this.lowerAction)); @@ -119,7 +138,7 @@ export class StepScalingPolicy extends Construct { this.upperAction = new StepScalingAction(this, 'UpperPolicy', { adjustmentType: props.adjustmentType, cooldown: props.cooldown, - metricAggregationType: aggregationTypeFromMetric(props.metric), + metricAggregationType: props.metricAggregationType ?? aggregationTypeFromMetric(props.metric), minAdjustmentMagnitude: props.minAdjustmentMagnitude, autoScalingGroup: props.autoScalingGroup, }); @@ -137,7 +156,7 @@ export class StepScalingPolicy extends Construct { metric: props.metric, alarmDescription: 'Upper threshold scaling alarm', comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, - evaluationPeriods: 1, + evaluationPeriods: props.evaluationPeriods ?? 1, threshold, }); this.upperAlarm.addAlarmAction(new StepScalingAlarmAction(this.upperAction)); @@ -157,7 +176,7 @@ function aggregationTypeFromMetric(metric: cloudwatch.IMetric): MetricAggregatio case 'Maximum': return MetricAggregationType.MAXIMUM; default: - throw new Error(`Cannot only scale on 'Minimum', 'Maximum', 'Average' metrics, got ${statistic}`); + return MetricAggregationType.AVERAGE; } } diff --git a/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts b/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts index b5f395540dcd0..d21ae8c3ec623 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts @@ -1,4 +1,4 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; @@ -273,6 +273,71 @@ nodeunitShim({ }, }); +test('step scaling from percentile metric', () => { + // GIVEN + const stack = new cdk.Stack(); + const fixture = new ASGFixture(stack, 'Fixture'); + + // WHEN + fixture.asg.scaleOnMetric('Tracking', { + metric: new cloudwatch.Metric({ namespace: 'Test', metricName: 'Metric', statistic: 'p99' }), + scalingSteps: [ + { upper: 0, change: -1 }, + { lower: 100, change: +1 }, + { lower: 500, change: +5 }, + ], + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AutoScaling::ScalingPolicy', { + PolicyType: 'StepScaling', + MetricAggregationType: 'Average', + })); + expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + ComparisonOperator: 'GreaterThanOrEqualToThreshold', + EvaluationPeriods: 1, + AlarmActions: [ + { Ref: 'FixtureASGTrackingUpperPolicy27D4301F' }, + ], + ExtendedStatistic: 'p99', + MetricName: 'Metric', + Namespace: 'Test', + Threshold: 100, + })); +}); + +test('step scaling with evaluation period configured', () => { + // GIVEN + const stack = new cdk.Stack(); + const fixture = new ASGFixture(stack, 'Fixture'); + + // WHEN + fixture.asg.scaleOnMetric('Tracking', { + metric: new cloudwatch.Metric({ namespace: 'Test', metricName: 'Metric', statistic: 'p99' }), + scalingSteps: [ + { upper: 0, change: -1 }, + { lower: 100, change: +1 }, + { lower: 500, change: +5 }, + ], + evaluationPeriods: 10, + metricAggregationType: autoscaling.MetricAggregationType.MAXIMUM, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AutoScaling::ScalingPolicy', { + PolicyType: 'StepScaling', + MetricAggregationType: 'Maximum', + })); + expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + ComparisonOperator: 'GreaterThanOrEqualToThreshold', + EvaluationPeriods: 10, + ExtendedStatistic: 'p99', + MetricName: 'Metric', + Namespace: 'Test', + Threshold: 100, + })); +}); + class ASGFixture extends Construct { public readonly vpc: ec2.Vpc; public readonly asg: autoscaling.AutoScalingGroup; diff --git a/packages/@aws-cdk/aws-batch/README.md b/packages/@aws-cdk/aws-batch/README.md index 99ea697ae4f28..48d5b7edf65d8 100644 --- a/packages/@aws-cdk/aws-batch/README.md +++ b/packages/@aws-cdk/aws-batch/README.md @@ -148,7 +148,7 @@ const computeEnv = batch.ComputeEnvironment.fromComputeEnvironmentArn(this, 'imp ### Change the baseline AMI of the compute resources -Ocassionally, you will need to deviate from the default processing AMI. +Occasionally, you will need to deviate from the default processing AMI. ECS Optimized Amazon Linux 2 example: @@ -186,7 +186,7 @@ const jobQueue = new batch.JobQueue(stack, 'JobQueue', { { // Defines a collection of compute resources to handle assigned batch jobs computeEnvironment, - // Order determines the allocation order for jobs (i.e. Lower means higher preferance for job assignment) + // Order determines the allocation order for jobs (i.e. Lower means higher preference for job assignment) order: 1, }, ], diff --git a/packages/@aws-cdk/aws-batch/lib/exposed-secret.ts b/packages/@aws-cdk/aws-batch/lib/exposed-secret.ts index 351095536add0..6ee6e3587208b 100644 --- a/packages/@aws-cdk/aws-batch/lib/exposed-secret.ts +++ b/packages/@aws-cdk/aws-batch/lib/exposed-secret.ts @@ -7,20 +7,20 @@ import * as ssm from '@aws-cdk/aws-ssm'; export class ExposedSecret { /** * Use Secrets Manager Secret - * @param optionaName - The name of the option + * @param optionName - The name of the option * @param secret - A secret from secrets manager */ - public static fromSecretsManager(optionaName: string, secret: secretsmanager.ISecret): ExposedSecret { - return new ExposedSecret(optionaName, secret.secretArn); + public static fromSecretsManager(optionName: string, secret: secretsmanager.ISecret): ExposedSecret { + return new ExposedSecret(optionName, secret.secretArn); } /** * User Parameters Store Parameter - * @param optionaName - The name of the option + * @param optionName - The name of the option * @param parameter - A parameter from parameters store */ - public static fromParametersStore(optionaName: string, parameter: ssm.IParameter): ExposedSecret { - return new ExposedSecret(optionaName, parameter.parameterArn); + public static fromParametersStore(optionName: string, parameter: ssm.IParameter): ExposedSecret { + return new ExposedSecret(optionName, parameter.parameterArn); } /** diff --git a/packages/@aws-cdk/aws-batch/lib/job-definition-image-config.ts b/packages/@aws-cdk/aws-batch/lib/job-definition-image-config.ts index 55b9b5c4ca86e..f212883ce07da 100644 --- a/packages/@aws-cdk/aws-batch/lib/job-definition-image-config.ts +++ b/packages/@aws-cdk/aws-batch/lib/job-definition-image-config.ts @@ -33,7 +33,7 @@ class TaskDefinition { } /** - * Internal function to allow the Batch Job task defintion + * Internal function to allow the Batch Job task definition * to match the CDK requirements of an EC2 task definition. * * @internal diff --git a/packages/@aws-cdk/aws-certificatemanager/README.md b/packages/@aws-cdk/aws-certificatemanager/README.md index 24202429ac5df..24f1e72a3cb20 100644 --- a/packages/@aws-cdk/aws-certificatemanager/README.md +++ b/packages/@aws-cdk/aws-certificatemanager/README.md @@ -102,7 +102,7 @@ new acm.Certificate(this, 'Certificate', { ## Cross-region Certificates ACM certificates that are used with CloudFront -- or higher-level constructs which rely on CloudFront -- must be in the `us-east-1` region. -The `DnsValidatedCertificate` construct exists to faciliate creating these certificates cross-region. This resource can only be used with +The `DnsValidatedCertificate` construct exists to facilitate creating these certificates cross-region. This resource can only be used with Route53-based DNS validation. ```ts diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts index df3d3988847ad..eac04dcb6df05 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts @@ -55,7 +55,7 @@ export interface CertificateProps { readonly validationMethod?: ValidationMethod; /** - * How to validate this certifcate + * How to validate this certificate * * @default CertificateValidation.fromEmail() */ @@ -100,7 +100,7 @@ export interface CertificationValidationProps { */ export class CertificateValidation { /** - * Validate the certifcate with DNS + * Validate the certificate with DNS * * IMPORTANT: If `hostedZone` is not specified, DNS records must be added * manually and the stack will not complete creating until the records are @@ -116,7 +116,7 @@ export class CertificateValidation { } /** - * Validate the certifcate with automatically created DNS records in multiple + * Validate the certificate with automatically created DNS records in multiple * Amazon Route 53 hosted zones. * * @param hostedZones a map of hosted zones where DNS records must be created @@ -130,7 +130,7 @@ export class CertificateValidation { } /** - * Validate the certifcate with Email + * Validate the certificate with Email * * IMPORTANT: if you are creating a certificate as part of your stack, the stack * will not complete creating until you read and follow the instructions in the diff --git a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts index aecf5d93c0d9a..75794f69ba5f9 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts +++ b/packages/@aws-cdk/aws-certificatemanager/lib/dns-validated-certificate.ts @@ -32,7 +32,7 @@ export interface DnsValidatedCertificateProps extends CertificateProps { * aws-cn partition, the default endpoint is not working now, hence the right endpoint * need to be specified through this prop. * - * Route53 is not been offically launched in China, it is only available for AWS + * Route53 is not been officially launched in China, it is only available for AWS * internal accounts now. To make DnsValidatedCertificate work for internal accounts * now, a special endpoint needs to be provided. * @@ -77,7 +77,7 @@ export class DnsValidatedCertificate extends cdk.Resource implements ICertificat const requestorFunction = new lambda.Function(this, 'CertificateRequestorFunction', { code: lambda.Code.fromAsset(path.resolve(__dirname, '..', 'lambda-packages', 'dns_validated_certificate_handler', 'lib')), handler: 'index.certificateRequestHandler', - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, timeout: cdk.Duration.minutes(15), role: props.customResourceRole, }); diff --git a/packages/@aws-cdk/aws-certificatemanager/test/dns-validated-certificate.test.ts b/packages/@aws-cdk/aws-certificatemanager/test/dns-validated-certificate.test.ts index 1e283f558880e..17e89b68e4343 100644 --- a/packages/@aws-cdk/aws-certificatemanager/test/dns-validated-certificate.test.ts +++ b/packages/@aws-cdk/aws-certificatemanager/test/dns-validated-certificate.test.ts @@ -31,7 +31,7 @@ test('creates CloudFormation Custom Resource', () => { }); expect(stack).toHaveResource('AWS::Lambda::Function', { Handler: 'index.certificateRequestHandler', - Runtime: 'nodejs10.x', + Runtime: 'nodejs14.x', Timeout: 900, }); expect(stack).toHaveResource('AWS::IAM::Policy', { diff --git a/packages/@aws-cdk/aws-chatbot/README.md b/packages/@aws-cdk/aws-chatbot/README.md index d46f0e704110e..2c4cee1196900 100644 --- a/packages/@aws-cdk/aws-chatbot/README.md +++ b/packages/@aws-cdk/aws-chatbot/README.md @@ -34,11 +34,6 @@ const slackChannel = new chatbot.SlackChannelConfiguration(this, 'MySlackChannel slackChannelId: 'YOUR_SLACK_CHANNEL_ID', }); -slackChannel.addLambdaInvokeCommandPermissions(); -slackChannel.addNotificationPermissions(); -slackChannel.addSupportCommandPermissions(); -slackChannel.addReadOnlyCommandPermissions(); - slackChannel.addToPrincipalPolicy(new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index d8c93f66aa910..ef97dc3d79c7f 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -257,7 +257,6 @@ export class Alarm extends AlarmBase { return dispatchMetric(metric, { withStat(stat, conf) { self.validateMetricStat(stat, metric); - if (conf.renderingProperties?.label == undefined) { return dropUndefined({ dimensions: stat.dimensions, @@ -283,7 +282,7 @@ export class Alarm extends AlarmBase { stat: stat.statistic, unit: stat.unitFilter, }, - id: stat.metricName, + id: 'm1', label: conf.renderingProperties?.label, returnData: true, } as CfnAlarm.MetricDataQueryProperty, diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-with-label.expected.json b/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-with-label.expected.json index 9ab6e14f29a6e..6ac734ed6e534 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-with-label.expected.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.alarm-with-label.expected.json @@ -5,7 +5,7 @@ "Properties": { "Metrics": [ { - "Id": "Metric", + "Id": "m1", "Label": "Metric [AVG: ${AVG}]", "MetricStat": { "Metric": { @@ -28,7 +28,7 @@ "Properties": { "Metrics": [ { - "Id": "Metric", + "Id": "m1", "Label": "Metric [AVG: ${AVG}]", "MetricStat": { "Metric": { diff --git a/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json b/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json index 5b4f51877ccfe..b4dfc40358bd1 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json +++ b/packages/@aws-cdk/aws-dynamodb-global/test/integ.dynamodb.global.expected.json @@ -196,7 +196,6 @@ ] } }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "SingletonLambdaD38B65A66B544FB69BAD9CD40A6DAC12ServiceRoleD9686810", @@ -205,6 +204,8 @@ }, "Runtime": "nodejs14.x", "Description": "Lambda to make DynamoDB a global table", + "Handler": "index.handler", + "Runtime": "nodejs14.x", "Timeout": 300 }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-dynamodb/README.md b/packages/@aws-cdk/aws-dynamodb/README.md index ac540dd11670c..2f43d83f88c4f 100644 --- a/packages/@aws-cdk/aws-dynamodb/README.md +++ b/packages/@aws-cdk/aws-dynamodb/README.md @@ -65,7 +65,7 @@ https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.Read You can have DynamoDB automatically raise and lower the read and write capacities of your table by setting up autoscaling. You can use this to either keep your -tables at a desired utilization level, or by scaling up and down at preconfigured +tables at a desired utilization level, or by scaling up and down at pre-configured times of the day: Auto-scaling is only relevant for tables with the billing mode, PROVISIONED. @@ -125,7 +125,7 @@ const globalTable = new dynamodb.Table(this, 'Table', { All user data stored in Amazon DynamoDB is fully encrypted at rest. When creating a new table, you can choose to encrypt using the following customer master keys (CMK) to encrypt your table: * AWS owned CMK - By default, all tables are encrypted under an AWS owned customer master key (CMK) in the DynamoDB service account (no additional charges apply). -* AWS managed CMK - AWS KMS keys (one per region) are created in your account, managed, and used on your behalf by AWS DynamoDB (AWS KMS chages apply). +* AWS managed CMK - AWS KMS keys (one per region) are created in your account, managed, and used on your behalf by AWS DynamoDB (AWS KMS charges apply). * Customer managed CMK - You have full control over the KMS key used to encrypt the DynamoDB Table (AWS KMS charges apply). Creating a Table encrypted with a customer managed CMK: diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 7ee8f81cb4b54..bbba9237b6432 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -1553,7 +1553,7 @@ export class Table extends TableBase { if (encryptionType === undefined) { encryptionType = props.encryptionKey != null - // If there is a configured encyptionKey, the encryption is implicitly CUSTOMER_MANAGED + // If there is a configured encryptionKey, the encryption is implicitly CUSTOMER_MANAGED ? TableEncryption.CUSTOMER_MANAGED // Otherwise, if severSideEncryption is enabled, it's AWS_MANAGED; else undefined (do not set anything) : props.serverSideEncryption ? TableEncryption.AWS_MANAGED : undefined; @@ -1611,7 +1611,7 @@ export enum AttributeType { } /** - * DyanmoDB's Read/Write capacity modes. + * DynamoDB's Read/Write capacity modes. */ export enum BillingMode { /** diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json index 2f49a9b48157d..879532d0a8879 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.dynamodb.sse.expected.json @@ -571,4 +571,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json index b4ea44f2709c4..af1e8defceed6 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global-replicas-provisioned.expected.json @@ -278,7 +278,7 @@ }, "/", { - "Ref": "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3Bucket806FEB2C" + "Ref": "AssetParameters9971e87ecc84219610f1dfbd0fbdd30e29f8d1f408df3f645299eb48b1c1ed00S3BucketC986830C" }, "/", { @@ -288,7 +288,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3VersionKey81C7BC5B" + "Ref": "AssetParameters9971e87ecc84219610f1dfbd0fbdd30e29f8d1f408df3f645299eb48b1c1ed00S3VersionKeyE0DA9F9E" } ] } @@ -301,7 +301,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3VersionKey81C7BC5B" + "Ref": "AssetParameters9971e87ecc84219610f1dfbd0fbdd30e29f8d1f408df3f645299eb48b1c1ed00S3VersionKeyE0DA9F9E" } ] } @@ -380,17 +380,17 @@ "Type": "String", "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" }, - "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3Bucket806FEB2C": { + "AssetParameters9971e87ecc84219610f1dfbd0fbdd30e29f8d1f408df3f645299eb48b1c1ed00S3BucketC986830C": { "Type": "String", - "Description": "S3 bucket for asset \"d56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aad\"" + "Description": "S3 bucket for asset \"9971e87ecc84219610f1dfbd0fbdd30e29f8d1f408df3f645299eb48b1c1ed00\"" }, - "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadS3VersionKey81C7BC5B": { + "AssetParameters9971e87ecc84219610f1dfbd0fbdd30e29f8d1f408df3f645299eb48b1c1ed00S3VersionKeyE0DA9F9E": { "Type": "String", - "Description": "S3 key for asset version \"d56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aad\"" + "Description": "S3 key for asset version \"9971e87ecc84219610f1dfbd0fbdd30e29f8d1f408df3f645299eb48b1c1ed00\"" }, - "AssetParametersd56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aadArtifactHashD0230F6F": { + "AssetParameters9971e87ecc84219610f1dfbd0fbdd30e29f8d1f408df3f645299eb48b1c1ed00ArtifactHash57FC5CA2": { "Type": "String", - "Description": "Artifact hash for asset \"d56d097acd2563516c51a0e04dcf8d9bf3638678f723d5b80f95d5c240836aad\"" + "Description": "Artifact hash for asset \"9971e87ecc84219610f1dfbd0fbdd30e29f8d1f408df3f645299eb48b1c1ed00\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json index 3896ac3a355b2..b1ca3b99819a0 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json +++ b/packages/@aws-cdk/aws-dynamodb/test/integ.global.expected.json @@ -253,7 +253,7 @@ }, "/", { - "Ref": "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3Bucket8BB0CECD" + "Ref": "AssetParameters96d72e249e15863715342dcc64ec41ea99be4dece8798e9e96a0da55763aa4b6S3BucketB5739B2A" }, "/", { @@ -263,7 +263,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3VersionKeyC531296D" + "Ref": "AssetParameters96d72e249e15863715342dcc64ec41ea99be4dece8798e9e96a0da55763aa4b6S3VersionKey5404A90E" } ] } @@ -276,7 +276,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3VersionKeyC531296D" + "Ref": "AssetParameters96d72e249e15863715342dcc64ec41ea99be4dece8798e9e96a0da55763aa4b6S3VersionKey5404A90E" } ] } @@ -329,17 +329,17 @@ "Type": "String", "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" }, - "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3Bucket8BB0CECD": { + "AssetParameters96d72e249e15863715342dcc64ec41ea99be4dece8798e9e96a0da55763aa4b6S3BucketB5739B2A": { "Type": "String", - "Description": "S3 bucket for asset \"a789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4\"" + "Description": "S3 bucket for asset \"96d72e249e15863715342dcc64ec41ea99be4dece8798e9e96a0da55763aa4b6\"" }, - "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4S3VersionKeyC531296D": { + "AssetParameters96d72e249e15863715342dcc64ec41ea99be4dece8798e9e96a0da55763aa4b6S3VersionKey5404A90E": { "Type": "String", - "Description": "S3 key for asset version \"a789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4\"" + "Description": "S3 key for asset version \"96d72e249e15863715342dcc64ec41ea99be4dece8798e9e96a0da55763aa4b6\"" }, - "AssetParametersa789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4ArtifactHash9D92B407": { + "AssetParameters96d72e249e15863715342dcc64ec41ea99be4dece8798e9e96a0da55763aa4b6ArtifactHash539C11C9": { "Type": "String", - "Description": "Artifact hash for asset \"a789639d6caa7a94b8135bc6ff3a6935f95624a9ed88014b5e7b3d340f20c3b4\"" + "Description": "Artifact hash for asset \"96d72e249e15863715342dcc64ec41ea99be4dece8798e9e96a0da55763aa4b6\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ec2/lib/launch-template.ts b/packages/@aws-cdk/aws-ec2/lib/launch-template.ts index 3b5b39f9b6370..fdc03755c0268 100644 --- a/packages/@aws-cdk/aws-ec2/lib/launch-template.ts +++ b/packages/@aws-cdk/aws-ec2/lib/launch-template.ts @@ -646,7 +646,7 @@ export class LaunchTemplate extends Resource implements ILaunchTemplate, iam.IGr */ public get connections(): Connections { if (!this._connections) { - throw new Error('LaunchTemplate can only be used as IConnectable if a securityGroup is provided when contructing it.'); + throw new Error('LaunchTemplate can only be used as IConnectable if a securityGroup is provided when constructing it.'); } return this._connections; } diff --git a/packages/@aws-cdk/aws-ec2/lib/network-util.ts b/packages/@aws-cdk/aws-ec2/lib/network-util.ts index 301c8ad5ed980..5c7a7c9b7027e 100644 --- a/packages/@aws-cdk/aws-ec2/lib/network-util.ts +++ b/packages/@aws-cdk/aws-ec2/lib/network-util.ts @@ -244,7 +244,7 @@ export class CidrBlock { } /* - * The maximum IP in the CIDR Blcok e.g. '10.0.8.255' + * The maximum IP in the CIDR Block e.g. '10.0.8.255' */ public maxIp(): string { // min + (2^(32-mask)) - 1 [zero needs to count] @@ -252,7 +252,7 @@ export class CidrBlock { } /* - * The minimum IP in the CIDR Blcok e.g. '10.0.0.0' + * The minimum IP in the CIDR Block e.g. '10.0.0.0' */ public minIp(): string { return NetworkUtils.numToIp(this.minAddress()); diff --git a/packages/@aws-cdk/aws-ec2/lib/port.ts b/packages/@aws-cdk/aws-ec2/lib/port.ts index 314c8d615b0dd..8436f3455cda1 100644 --- a/packages/@aws-cdk/aws-ec2/lib/port.ts +++ b/packages/@aws-cdk/aws-ec2/lib/port.ts @@ -2,17 +2,158 @@ import { Token } from '@aws-cdk/core'; /** * Protocol for use in Connection Rules + * + * https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml */ export enum Protocol { ALL = '-1', + HOPOPT = '0', + ICMP = 'icmp', + IGMP = '2', + GGP = '3', + IPV4 = '4', + ST = '5', TCP = 'tcp', + CBT = '7', + EGP = '8', + IGP = '9', + BBN_RCC_MON = '10', + NVP_II = '11', + PUP = '12', + EMCON = '14', + XNET = '15', + CHAOS = '16', UDP = 'udp', - ICMP = 'icmp', - ICMPV6 = '58', - ESP = 'esp', - AH = 'ah', + MUX = '18', + DCN_MEAS = '19', + HMP = '20', + PRM = '21', + XNS_IDP = '22', + TRUNK_1 = '23', + TRUNK_2 = '24', + LEAF_1 = '25', + LEAF_2 = '26', + RDP = '27', + IRTP = '28', + ISO_TP4 = '29', + NETBLT = '30', + MFE_NSP = '31', + MERIT_INP = '32', + DCCP = '33', + THREEPC = '34', + IDPR = '35', + XTP = '36', + DDP = '37', + IDPR_CMTP = '38', + TPPLUSPLUS = '39', + IL = '40', + IPV6 = '41', + SDRP = '42', + IPV6_ROUTE = '43', + IPV6_FRAG = '44', + IDRP = '45', + RSVP = '46', + GRE = '47', + DSR = '48', + BNA = '49', + ESP = '50', + AH = '51', + I_NLSP = '52', + SWIPE = '53', + NARP = '54', + MOBILE = '55', + TLSP = '56', + SKIP = '57', + ICMPV6 = 'icmpv6', + IPV6_NONXT = '59', + IPV6_OPTS = '60', + CFTP = '62', + ANY_LOCAL = '63', + SAT_EXPAK = '64', + KRYPTOLAN = '65', + RVD = '66', + IPPC = '67', + ANY_DFS = '68', + SAT_MON = '69', + VISA = '70', + IPCV = '71', + CPNX = '72', + CPHB = '73', + WSN = '74', + PVP = '75', + BR_SAT_MON = '76', + SUN_ND = '77', + WB_MON = '78', + WB_EXPAK = '79', + ISO_IP = '80', + VMTP = '81', + SECURE_VMTP = '82', + VINES = '83', + TTP = '84', + IPTM = '84', + NSFNET_IGP = '85', + DGP = '86', + TCF = '87', + EIGRP = '88', + OSPFIGP = '89', + SPRITE_RPC = '90', + LARP = '91', + MTP = '92', + AX_25 = '93', + IPIP = '94', + MICP = '95', + SCC_SP = '96', + ETHERIP = '97', + ENCAP = '98', + ANY_ENC = '99', + GMTP = '100', + IFMP = '101', + PNNI = '102', + PIM = '103', + ARIS = '104', + SCPS = '105', + QNX = '106', + A_N = '107', + IPCOMP = '108', + SNP = '109', + COMPAQ_PEER = '110', + IPX_IN_IP = '111', + VRRP = '112', + PGM = '113', + ANY_0_HOP = '114', + L2_T_P = '115', + DDX = '116', + IATP = '117', + STP = '118', + SRP = '119', + UTI = '120', + SMP = '121', + SM = '122', + PTP = '123', + ISIS_IPV4 = '124', + FIRE = '125', + CRTP = '126', + CRUDP = '127', + SSCOPMCE = '128', + IPLT = '129', + SPS = '130', + PIPE = '131', + SCTP = '132', + FC = '133', + RSVP_E2E_IGNORE = '134', + MOBILITY_HEADER = '135', + UDPLITE = '136', + MPLS_IN_IP = '137', + MANET = '138', + HIP = '139', + SHIM6 = '140', + WESP = '141', + ROHC = '142', + ETHERNET = '143', + EXPERIMENT_1 = '253', + EXPERIMENT_2 = '254', + RESERVED = '255', } - /** * Properties to create a port range */ diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index f8619dd614adc..09d2819b3f0cf 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -1593,7 +1593,7 @@ export class Subnet extends Resource implements ISubnet { } /** - * Adds an entry to this subnets route table that points to the passed NATGatwayId + * Adds an entry to this subnets route table that points to the passed NATGatewayId * @param natGatewayId The ID of the NAT gateway */ public addDefaultNatRoute(natGatewayId: string) { diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index c32063ef1ca58..2a887208d9304 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -315,13 +315,153 @@ "docs-public-apis:@aws-cdk/aws-ec2.AmazonLinuxStorage", "docs-public-apis:@aws-cdk/aws-ec2.OperatingSystemType.LINUX", "docs-public-apis:@aws-cdk/aws-ec2.OperatingSystemType.WINDOWS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.AH", "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ALL", - "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TCP", - "docs-public-apis:@aws-cdk/aws-ec2.Protocol.UDP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ANY_0_HOP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ANY_DFS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ANY_ENC", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ANY_LOCAL", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ARIS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.AX_25", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.A_N", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.BBN_RCC_MON", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.BNA", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.BR_SAT_MON", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.CBT", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.CFTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.CHAOS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.COMPAQ_PEER", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.CPHB", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.CPNX", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.CRTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.CRUDP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.DCCP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.DCN_MEAS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.DDP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.DDX", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.DGP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.DSR", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.EGP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.EIGRP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.EMCON", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ENCAP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ESP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ETHERIP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ETHERNET", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.EXPERIMENT_1", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.EXPERIMENT_2", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.FC", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.FIRE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.GGP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.GMTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.GRE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.HIP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.HMP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.HOPOPT", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IATP", "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ICMP", "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ICMPV6", - "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ESP", - "docs-public-apis:@aws-cdk/aws-ec2.Protocol.AH", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IDPR", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IDPR_CMTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IDRP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IFMP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IGMP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IGP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IL", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPCOMP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPCV", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPIP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPLT", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPPC", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPTM", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPV4", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPV6", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPV6_FRAG", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPV6_NONXT", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPV6_OPTS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPV6_ROUTE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPX_IN_IP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IRTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ISIS_IPV4", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ISO_IP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ISO_TP4", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.I_NLSP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.KRYPTOLAN", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.L2_T_P", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.LARP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.LEAF_1", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.LEAF_2", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MANET", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MERIT_INP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MFE_NSP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MICP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MOBILE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MOBILITY_HEADER", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MPLS_IN_IP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MUX", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.NARP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.NETBLT", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.NSFNET_IGP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.NVP_II", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.OSPFIGP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PGM", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PIM", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PIPE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PNNI", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PRM", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PUP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PVP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.QNX", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.RDP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.RESERVED", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ROHC", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.RSVP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.RSVP_E2E_IGNORE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.RVD", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SAT_EXPAK", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SAT_MON", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SCC_SP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SCPS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SCTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SDRP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SECURE_VMTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SHIM6", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SKIP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SM", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SMP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SNP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SPRITE_RPC", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SPS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SRP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SSCOPMCE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ST", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.STP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SUN_ND", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SWIPE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TCF", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TCP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.THREEPC", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TLSP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TPPLUSPLUS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TRUNK_1", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TRUNK_2", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.UDP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.UDPLITE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.UTI", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.VINES", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.VISA", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.VMTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.VRRP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.WB_EXPAK", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.WB_MON", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.WESP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.WSN", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.XNET", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.XNS_IDP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.XTP", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_SP2_ENGLISH_64BIT_SQL_2008_SP4_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_CHINESE_SIMPLIFIED_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_CHINESE_TRADITIONAL_64BIT_BASE", diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json index 641b97b4ddbd5..8aad8918d8ace 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json @@ -567,20 +567,6 @@ "FromPort": 800, "IpProtocol": "udp", "ToPort": 801 - }, - { - "CidrIp": "0.0.0.0/0", - "Description": "from 0.0.0.0/0:ESP 50", - "FromPort": 50, - "IpProtocol": "esp", - "ToPort": 50 - }, - { - "CidrIp": "0.0.0.0/0", - "Description": "from 0.0.0.0/0:AH 51", - "FromPort": 51, - "IpProtocol": "ah", - "ToPort": 51 } ], "VpcId": { diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts b/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts index 88e4dacf9839a..2ffd5653e33f4 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts @@ -16,8 +16,6 @@ const rules = [ ec2.Port.allUdp(), ec2.Port.udp(123), ec2.Port.udpRange(800, 801), - ec2.Port.esp(), - ec2.Port.ah(), ]; for (const rule of rules) { diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index a1072d47fe700..2293c129ee245 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -394,6 +394,25 @@ const loadBalancedFargateService = new ApplicationLoadBalancedFargateService(sta }); ``` +### Deployment circuit breaker and rollback + +Amazon ECS [deployment circuit breaker](https://aws.amazon.com/tw/blogs/containers/announcing-amazon-ecs-deployment-circuit-breaker/) +automatically rolls back unhealthy service deployments without the need for manual intervention. Use `circuitBreaker` to enable +deployment circuit breaker and optionally enable `rollback` for automatic rollback. See [Using the deployment circuit breaker](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-ecs.html) +for more details. + +```ts +const service = new ApplicationLoadBalancedFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 1024, + desiredCount: 1, + cpu: 512, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + }, + circuitBreaker: { rollback: true }, +}); +``` ### Set deployment configuration on QueueProcessingService @@ -469,7 +488,7 @@ const scheduledFargateTask = new ScheduledFargateTask(stack, 'ScheduledFargateTa ### Use the REMOVE_DEFAULT_DESIRED_COUNT feature flag -The REMOVE_DEFAULT_DESIRED_COUNT feature flag is used to override the default desiredCount that is autogenerated by the CDK. This will set the desiredCount of any service created by any of the following constructs to be undefined. +The REMOVE_DEFAULT_DESIRED_COUNT feature flag is used to override the default desiredCount that is autogenerated by the CDK. This will set the desiredCount of any service created by any of the following constructs to be undefined. * ApplicationLoadBalancedEc2Service * ApplicationLoadBalancedFargateService diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts index 2ffa03620d672..a380d77a2a91f 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts @@ -1,6 +1,9 @@ import { Certificate, CertificateValidation, ICertificate } from '@aws-cdk/aws-certificatemanager'; import { IVpc } from '@aws-cdk/aws-ec2'; -import { AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerImage, DeploymentController, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; +import { + AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerImage, DeploymentController, DeploymentCircuitBreaker, + ICluster, LogDriver, PropagatedTagSource, Secret, +} from '@aws-cdk/aws-ecs'; import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, IApplicationLoadBalancer, ListenerCertificate, ListenerAction, AddApplicationTargetsProps, @@ -222,6 +225,14 @@ export interface ApplicationLoadBalancedServiceBaseProps { * @default - Rolling update (ECS) */ readonly deploymentController?: DeploymentController; + + /** + * Whether to enable the deployment circuit breaker. If this property is defined, circuit breaker will be implicitly + * enabled. + * @default - disabled + */ + readonly circuitBreaker?: DeploymentCircuitBreaker; + } export interface ApplicationLoadBalancedTaskImageOptions { diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts index 4f73da78e1e70..f04ca581f14cb 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts @@ -1,5 +1,8 @@ import { IVpc } from '@aws-cdk/aws-ec2'; -import { AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerImage, DeploymentController, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; +import { + AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerImage, DeploymentController, DeploymentCircuitBreaker, + ICluster, LogDriver, PropagatedTagSource, Secret, +} from '@aws-cdk/aws-ecs'; import { INetworkLoadBalancer, NetworkListener, NetworkLoadBalancer, NetworkTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; import { IRole } from '@aws-cdk/aws-iam'; import { ARecord, CnameRecord, IHostedZone, RecordTarget } from '@aws-cdk/aws-route53'; @@ -172,6 +175,13 @@ export interface NetworkLoadBalancedServiceBaseProps { * @default - Rolling update (ECS) */ readonly deploymentController?: DeploymentController; + + /** + * Whether to enable the deployment circuit breaker. If this property is defined, circuit breaker will be implicitly + * enabled. + * @default - disabled + */ + readonly circuitBreaker?: DeploymentCircuitBreaker; } export interface NetworkLoadBalancedTaskImageOptions { 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 733bc8f043761..6bad39534ccc4 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 @@ -1,6 +1,9 @@ import { ScalingInterval } from '@aws-cdk/aws-applicationautoscaling'; import { IVpc } from '@aws-cdk/aws-ec2'; -import { AwsLogDriver, BaseService, Cluster, ContainerImage, DeploymentController, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; +import { + AwsLogDriver, BaseService, Cluster, ContainerImage, DeploymentController, DeploymentCircuitBreaker, + ICluster, LogDriver, PropagatedTagSource, Secret, +} from '@aws-cdk/aws-ecs'; import { IQueue, Queue } from '@aws-cdk/aws-sqs'; import { CfnOutput, Duration, Stack } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; @@ -185,6 +188,13 @@ export interface QueueProcessingServiceBaseProps { * @default - Rolling update (ECS) */ readonly deploymentController?: DeploymentController; + + /** + * Whether to enable the deployment circuit breaker. If this property is defined, circuit breaker will be implicitly + * enabled. + * @default - disabled + */ + readonly circuitBreaker?: DeploymentCircuitBreaker; } /** diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts index 2915fce6a48ff..ed606bb72c907 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/application-load-balanced-ecs-service.ts @@ -133,6 +133,7 @@ export class ApplicationLoadBalancedEc2Service extends ApplicationLoadBalancedSe enableECSManagedTags: props.enableECSManagedTags, cloudMapOptions: props.cloudMapOptions, deploymentController: props.deploymentController, + circuitBreaker: props.circuitBreaker, }); this.addServiceAsTarget(this.service); } diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts index 881a346c74f8a..4bae918ca67fc 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/network-load-balanced-ecs-service.ts @@ -131,6 +131,7 @@ export class NetworkLoadBalancedEc2Service extends NetworkLoadBalancedServiceBas enableECSManagedTags: props.enableECSManagedTags, cloudMapOptions: props.cloudMapOptions, deploymentController: props.deploymentController, + circuitBreaker: props.circuitBreaker, }); this.addServiceAsTarget(this.service); } 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 6858813dfa9cc..985a4d2390e6b 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 @@ -114,6 +114,7 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { propagateTags: props.propagateTags, enableECSManagedTags: props.enableECSManagedTags, deploymentController: props.deploymentController, + circuitBreaker: props.circuitBreaker, }); this.configureAutoscalingForService(this.service); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts index fbb68aef84b2f..326a68529272b 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/application-load-balanced-fargate-service.ts @@ -170,6 +170,7 @@ export class ApplicationLoadBalancedFargateService extends ApplicationLoadBalanc cloudMapOptions: props.cloudMapOptions, platformVersion: props.platformVersion, deploymentController: props.deploymentController, + circuitBreaker: props.circuitBreaker, securityGroups: props.securityGroups, vpcSubnets: props.taskSubnets, }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts index 404d5429acfed..1f2618bbca314 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/network-load-balanced-fargate-service.ts @@ -157,6 +157,7 @@ export class NetworkLoadBalancedFargateService extends NetworkLoadBalancedServic cloudMapOptions: props.cloudMapOptions, platformVersion: props.platformVersion, deploymentController: props.deploymentController, + circuitBreaker: props.circuitBreaker, vpcSubnets: props.taskSubnets, }); this.addServiceAsTarget(this.service); 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 6444d05f81da6..cb4b8d77a8188 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 @@ -148,6 +148,7 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { securityGroups: props.securityGroups, vpcSubnets: props.taskSubnets, assignPublicIp: props.assignPublicIp, + circuitBreaker: props.circuitBreaker, }); this.configureAutoscalingForService(this.service); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts index 6f5a4aa0b3337..d41a7684fd677 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts @@ -1098,6 +1098,72 @@ export = { test.done(); }, + 'ALB with circuit breaker'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedEc2Service(stack, 'Service', { + cluster, + memoryLimitMiB: 1024, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }, + circuitBreaker: { rollback: true }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + DeploymentConfiguration: { + DeploymentCircuitBreaker: { + Enable: true, + Rollback: true, + }, + }, + DeploymentController: { + Type: 'ECS', + }, + })); + + test.done(); + }, + + 'NLB with circuit breaker'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + + // WHEN + new ecsPatterns.NetworkLoadBalancedEc2Service(stack, 'Service', { + cluster, + memoryLimitMiB: 1024, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }, + circuitBreaker: { rollback: true }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + DeploymentConfiguration: { + DeploymentCircuitBreaker: { + Enable: true, + Rollback: true, + }, + }, + DeploymentController: { + Type: 'ECS', + }, + })); + + test.done(); + }, + 'NetworkLoadbalancedEC2Service accepts previously created load balancer'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts index 2c410f6581b1e..77d822d443048 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 @@ -209,9 +209,7 @@ export = { maxHealthyPercent: 150, serviceName: 'ecs-test-service', family: 'ecs-task-family', - deploymentController: { - type: ecs.DeploymentControllerType.CODE_DEPLOY, - }, + circuitBreaker: { rollback: true }, }); // THEN - QueueWorker is of EC2 launch type, an SQS queue is created and all optional properties are set. @@ -220,11 +218,15 @@ export = { DeploymentConfiguration: { MinimumHealthyPercent: 60, MaximumPercent: 150, + DeploymentCircuitBreaker: { + Enable: true, + Rollback: true, + }, }, LaunchType: 'EC2', ServiceName: 'ecs-test-service', DeploymentController: { - Type: 'CODE_DEPLOY', + Type: 'ECS', }, })); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.circuit-breaker-load-balanced-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.circuit-breaker-load-balanced-fargate-service.expected.json new file mode 100644 index 0000000000000..19af7964b18be --- /dev/null +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.circuit-breaker-load-balanced-fargate-service.expected.json @@ -0,0 +1,702 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "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-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "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-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "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-ecs-integ/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.192.0/18", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "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-ecs-integ/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-integ/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "ClusterEB0386A7": { + "Type": "AWS::ECS::Cluster" + }, + "myServiceLB168895E1": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "LoadBalancerAttributes": [ + { + "Key": "deletion_protection.enabled", + "Value": "false" + } + ], + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "myServiceLBSecurityGroupFE0ED608", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + ], + "Type": "application" + }, + "DependsOn": [ + "VpcPublicSubnet1DefaultRoute3DA9E72A", + "VpcPublicSubnet2DefaultRoute97F91067" + ] + }, + "myServiceLBSecurityGroupFE0ED608": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB awsecsintegmyServiceLB1F7A535D", + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow from anyone on port 80", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "myServiceLBSecurityGrouptoawsecsintegmyServiceSecurityGroup8DAB521180B6703B07": { + "Type": "AWS::EC2::SecurityGroupEgress", + "Properties": { + "GroupId": { + "Fn::GetAtt": [ + "myServiceLBSecurityGroupFE0ED608", + "GroupId" + ] + }, + "IpProtocol": "tcp", + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "myServiceSecurityGroupC3B9D4E0", + "GroupId" + ] + }, + "FromPort": 80, + "ToPort": 80 + } + }, + "myServiceLBPublicListenerC78AE8A0": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "myServiceLBPublicListenerECSGroup17E9BBC1" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "myServiceLB168895E1" + }, + "Port": 80, + "Protocol": "HTTP" + } + }, + "myServiceLBPublicListenerECSGroup17E9BBC1": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "TargetType": "ip", + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "myServiceTaskDefTaskRole1C1DE6CC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "myServiceTaskDef7FB8322A": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "amazon/amazon-ecs-sample", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "myServiceTaskDefwebLogGroupA1767F2C" + }, + "awslogs-stream-prefix": "myService", + "awslogs-region": { + "Ref": "AWS::Region" + } + } + }, + "Name": "web", + "PortMappings": [ + { + "ContainerPort": 80, + "Protocol": "tcp" + } + ] + } + ], + "Cpu": "256", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "myServiceTaskDefExecutionRole618CD311", + "Arn" + ] + }, + "Family": "awsecsintegmyServiceTaskDefA3A33D18", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "myServiceTaskDefTaskRole1C1DE6CC", + "Arn" + ] + } + } + }, + "myServiceTaskDefwebLogGroupA1767F2C": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "myServiceTaskDefExecutionRole618CD311": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "myServiceTaskDefExecutionRoleDefaultPolicyBDAEC571": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "myServiceTaskDefwebLogGroupA1767F2C", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "myServiceTaskDefExecutionRoleDefaultPolicyBDAEC571", + "Roles": [ + { + "Ref": "myServiceTaskDefExecutionRole618CD311" + } + ] + } + }, + "myServiceB0B6FAA0": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "ClusterEB0386A7" + }, + "DeploymentConfiguration": { + "DeploymentCircuitBreaker": { + "Enable": true, + "Rollback": true + }, + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DeploymentController": { + "Type": "ECS" + }, + "EnableECSManagedTags": false, + "HealthCheckGracePeriodSeconds": 60, + "LaunchType": "FARGATE", + "LoadBalancers": [ + { + "ContainerName": "web", + "ContainerPort": 80, + "TargetGroupArn": { + "Ref": "myServiceLBPublicListenerECSGroup17E9BBC1" + } + } + ], + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "myServiceSecurityGroupC3B9D4E0", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + ] + } + }, + "TaskDefinition": { + "Ref": "myServiceTaskDef7FB8322A" + } + }, + "DependsOn": [ + "myServiceLBPublicListenerECSGroup17E9BBC1", + "myServiceLBPublicListenerC78AE8A0" + ] + }, + "myServiceSecurityGroupC3B9D4E0": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-integ/myService/Service/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "myServiceSecurityGroupfromawsecsintegmyServiceLBSecurityGroupFA544FE5800A81885C": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "Load balancer to target", + "FromPort": 80, + "GroupId": { + "Fn::GetAtt": [ + "myServiceSecurityGroupC3B9D4E0", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "myServiceLBSecurityGroupFE0ED608", + "GroupId" + ] + }, + "ToPort": 80 + } + } + }, + "Outputs": { + "myServiceLoadBalancerDNS3A083E9F": { + "Value": { + "Fn::GetAtt": [ + "myServiceLB168895E1", + "DNSName" + ] + } + }, + "myServiceServiceURL1258C56B": { + "Value": { + "Fn::Join":[ + "", + [ + "http://", + { + "Fn::GetAtt": [ + "myServiceLB168895E1", + "DNSName" + ] + } + ] + ] + } + } + } +} diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.circuit-breaker-load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.circuit-breaker-load-balanced-fargate-service.ts new file mode 100644 index 0000000000000..57eeb8db17d5b --- /dev/null +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.circuit-breaker-load-balanced-fargate-service.ts @@ -0,0 +1,21 @@ +import { Vpc } from '@aws-cdk/aws-ec2'; +import { Cluster, ContainerImage } from '@aws-cdk/aws-ecs'; +import { App, Stack } from '@aws-cdk/core'; + +import { ApplicationLoadBalancedFargateService } from '../../lib'; + +const app = new App(); +const stack = new Stack(app, 'aws-ecs-integ'); +const vpc = new Vpc(stack, 'Vpc', { maxAzs: 2 }); +const cluster = new Cluster(stack, 'Cluster', { vpc }); + +new ApplicationLoadBalancedFargateService(stack, 'myService', { + cluster, + memoryLimitMiB: 512, + taskImageOptions: { + image: ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }, + circuitBreaker: { rollback: true }, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.circuit-breaker-queue-processing-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.circuit-breaker-queue-processing-fargate-service.expected.json new file mode 100644 index 0000000000000..3b8fad89fcf97 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.circuit-breaker-queue-processing-fargate-service.expected.json @@ -0,0 +1,848 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-patterns-queue/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-ecs-patterns-queue/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-patterns-queue/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-ecs-patterns-queue/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-patterns-queue/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-ecs-patterns-queue/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-patterns-queue/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-ecs-patterns-queue/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-patterns-queue/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-ecs-patterns-queue/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-patterns-queue/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-ecs-patterns-queue/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-ecs-patterns-queue/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-ecs-patterns-queue/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "QueueProcessingServiceEcsProcessingDeadLetterQueueD47A7C6B": { + "Type": "AWS::SQS::Queue", + "Properties": { + "MessageRetentionPeriod": 1209600 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "QueueProcessingServiceEcsProcessingQueue552F0B37": { + "Type": "AWS::SQS::Queue", + "Properties": { + "RedrivePolicy": { + "deadLetterTargetArn": { + "Fn::GetAtt": [ + "QueueProcessingServiceEcsProcessingDeadLetterQueueD47A7C6B", + "Arn" + ] + }, + "maxReceiveCount": 3 + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "QueueProcessingServiceQueueProcessingTaskDefTaskRole782B79A6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "QueueProcessingServiceQueueProcessingTaskDefTaskRoleDefaultPolicyAE808B19": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "sqs:ReceiveMessage", + "sqs:ChangeMessageVisibility", + "sqs:GetQueueUrl", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QueueProcessingServiceEcsProcessingQueue552F0B37", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QueueProcessingServiceQueueProcessingTaskDefTaskRoleDefaultPolicyAE808B19", + "Roles": [ + { + "Ref": "QueueProcessingServiceQueueProcessingTaskDefTaskRole782B79A6" + } + ] + } + }, + "QueueProcessingServiceQueueProcessingTaskDef4982F68B": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "QUEUE_NAME", + "Value": { + "Fn::GetAtt": [ + "QueueProcessingServiceEcsProcessingQueue552F0B37", + "QueueName" + ] + } + } + ], + "Essential": true, + "Image": { + "Fn::Join": [ + "", + [ + { + "Ref": "AWS::AccountId" + }, + ".dkr.ecr.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/aws-cdk/assets:3a8ba3ad06ed212b075efa3157fb407649c5996812bc64eeb5209e220aab4be5" + ] + ] + }, + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "QueueProcessingServiceQueueProcessingTaskDefQueueProcessingContainerLogGroupCC92448A" + }, + "awslogs-stream-prefix": "QueueProcessingService", + "awslogs-region": { + "Ref": "AWS::Region" + } + } + }, + "Name": "QueueProcessingContainer" + } + ], + "Cpu": "256", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "QueueProcessingServiceQueueProcessingTaskDefExecutionRole37838985", + "Arn" + ] + }, + "Family": "awsecspatternsqueueQueueProcessingServiceQueueProcessingTaskDef2D9F8C2B", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "QueueProcessingServiceQueueProcessingTaskDefTaskRole782B79A6", + "Arn" + ] + } + } + }, + "QueueProcessingServiceQueueProcessingTaskDefQueueProcessingContainerLogGroupCC92448A": { + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "QueueProcessingServiceQueueProcessingTaskDefExecutionRole37838985": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "QueueProcessingServiceQueueProcessingTaskDefExecutionRoleDefaultPolicyA83D332D": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ecr:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":repository/aws-cdk/assets" + ] + ] + } + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "QueueProcessingServiceQueueProcessingTaskDefQueueProcessingContainerLogGroupCC92448A", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "QueueProcessingServiceQueueProcessingTaskDefExecutionRoleDefaultPolicyA83D332D", + "Roles": [ + { + "Ref": "QueueProcessingServiceQueueProcessingTaskDefExecutionRole37838985" + } + ] + } + }, + "QueueProcessingServiceQueueProcessingFargateService0340DB9F": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Ref": "EcsDefaultClusterMnL3mNNYNVPC9C1EC7A3" + }, + "DeploymentConfiguration": { + "DeploymentCircuitBreaker": { + "Enable": true, + "Rollback": true + }, + "MaximumPercent": 200, + "MinimumHealthyPercent": 50 + }, + "DeploymentController": { + "Type": "ECS" + }, + "EnableECSManagedTags": false, + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "QueueProcessingServiceQueueProcessingFargateServiceSecurityGroup8FDF413D", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + ] + } + }, + "TaskDefinition": { + "Ref": "QueueProcessingServiceQueueProcessingTaskDef4982F68B" + } + } + }, + "QueueProcessingServiceQueueProcessingFargateServiceSecurityGroup8FDF413D": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-ecs-patterns-queue/QueueProcessingService/QueueProcessingFargateService/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "QueueProcessingServiceQueueProcessingFargateServiceTaskCountTargetA9D54444": { + "Type": "AWS::ApplicationAutoScaling::ScalableTarget", + "Properties": { + "MaxCapacity": 2, + "MinCapacity": 1, + "ResourceId": { + "Fn::Join": [ + "", + [ + "service/", + { + "Ref": "EcsDefaultClusterMnL3mNNYNVPC9C1EC7A3" + }, + "/", + { + "Fn::GetAtt": [ + "QueueProcessingServiceQueueProcessingFargateService0340DB9F", + "Name" + ] + } + ] + ] + }, + "RoleARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService" + ] + ] + }, + "ScalableDimension": "ecs:service:DesiredCount", + "ServiceNamespace": "ecs" + } + }, + "QueueProcessingServiceQueueProcessingFargateServiceTaskCountTargetCpuScaling330150E9": { + "Type": "AWS::ApplicationAutoScaling::ScalingPolicy", + "Properties": { + "PolicyName": "awsecspatternsqueueQueueProcessingServiceQueueProcessingFargateServiceTaskCountTargetCpuScaling374CE648", + "PolicyType": "TargetTrackingScaling", + "ScalingTargetId": { + "Ref": "QueueProcessingServiceQueueProcessingFargateServiceTaskCountTargetA9D54444" + }, + "TargetTrackingScalingPolicyConfiguration": { + "PredefinedMetricSpecification": { + "PredefinedMetricType": "ECSServiceAverageCPUUtilization" + }, + "TargetValue": 50 + } + } + }, + "QueueProcessingServiceQueueProcessingFargateServiceTaskCountTargetQueueMessagesVisibleScalingLowerPolicy332E2644": { + "Type": "AWS::ApplicationAutoScaling::ScalingPolicy", + "Properties": { + "PolicyName": "awsecspatternsqueueQueueProcessingServiceQueueProcessingFargateServiceTaskCountTargetQueueMessagesVisibleScalingLowerPolicy74582401", + "PolicyType": "StepScaling", + "ScalingTargetId": { + "Ref": "QueueProcessingServiceQueueProcessingFargateServiceTaskCountTargetA9D54444" + }, + "StepScalingPolicyConfiguration": { + "AdjustmentType": "ChangeInCapacity", + "MetricAggregationType": "Maximum", + "StepAdjustments": [ + { + "MetricIntervalUpperBound": 0, + "ScalingAdjustment": -1 + } + ] + } + } + }, + "QueueProcessingServiceQueueProcessingFargateServiceTaskCountTargetQueueMessagesVisibleScalingLowerAlarm20C30A06": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "LessThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmActions": [ + { + "Ref": "QueueProcessingServiceQueueProcessingFargateServiceTaskCountTargetQueueMessagesVisibleScalingLowerPolicy332E2644" + } + ], + "AlarmDescription": "Lower threshold scaling alarm", + "Dimensions": [ + { + "Name": "QueueName", + "Value": { + "Fn::GetAtt": [ + "QueueProcessingServiceEcsProcessingQueue552F0B37", + "QueueName" + ] + } + } + ], + "MetricName": "ApproximateNumberOfMessagesVisible", + "Namespace": "AWS/SQS", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 0 + } + }, + "QueueProcessingServiceQueueProcessingFargateServiceTaskCountTargetQueueMessagesVisibleScalingUpperPolicy84DD739A": { + "Type": "AWS::ApplicationAutoScaling::ScalingPolicy", + "Properties": { + "PolicyName": "awsecspatternsqueueQueueProcessingServiceQueueProcessingFargateServiceTaskCountTargetQueueMessagesVisibleScalingUpperPolicy23C5F983", + "PolicyType": "StepScaling", + "ScalingTargetId": { + "Ref": "QueueProcessingServiceQueueProcessingFargateServiceTaskCountTargetA9D54444" + }, + "StepScalingPolicyConfiguration": { + "AdjustmentType": "ChangeInCapacity", + "MetricAggregationType": "Maximum", + "StepAdjustments": [ + { + "MetricIntervalLowerBound": 0, + "MetricIntervalUpperBound": 400, + "ScalingAdjustment": 1 + }, + { + "MetricIntervalLowerBound": 400, + "ScalingAdjustment": 5 + } + ] + } + } + }, + "QueueProcessingServiceQueueProcessingFargateServiceTaskCountTargetQueueMessagesVisibleScalingUpperAlarm2660BEDF": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "AlarmActions": [ + { + "Ref": "QueueProcessingServiceQueueProcessingFargateServiceTaskCountTargetQueueMessagesVisibleScalingUpperPolicy84DD739A" + } + ], + "AlarmDescription": "Upper threshold scaling alarm", + "Dimensions": [ + { + "Name": "QueueName", + "Value": { + "Fn::GetAtt": [ + "QueueProcessingServiceEcsProcessingQueue552F0B37", + "QueueName" + ] + } + } + ], + "MetricName": "ApproximateNumberOfMessagesVisible", + "Namespace": "AWS/SQS", + "Period": 300, + "Statistic": "Maximum", + "Threshold": 100 + } + }, + "EcsDefaultClusterMnL3mNNYNVPC9C1EC7A3": { + "Type": "AWS::ECS::Cluster" + } + }, + "Outputs": { + "QueueProcessingServiceSQSDeadLetterQueueE9058015": { + "Value": { + "Fn::GetAtt": [ + "QueueProcessingServiceEcsProcessingDeadLetterQueueD47A7C6B", + "QueueName" + ] + } + }, + "QueueProcessingServiceSQSDeadLetterQueueArnF7C6D3A8": { + "Value": { + "Fn::GetAtt": [ + "QueueProcessingServiceEcsProcessingDeadLetterQueueD47A7C6B", + "Arn" + ] + } + }, + "QueueProcessingServiceSQSQueue1AD9CD9C": { + "Value": { + "Fn::GetAtt": [ + "QueueProcessingServiceEcsProcessingQueue552F0B37", + "QueueName" + ] + } + }, + "QueueProcessingServiceSQSQueueArn8C4AE4AE": { + "Value": { + "Fn::GetAtt": [ + "QueueProcessingServiceEcsProcessingQueue552F0B37", + "Arn" + ] + } + } + } +} diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.circuit-breaker-queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.circuit-breaker-queue-processing-fargate-service.ts new file mode 100644 index 0000000000000..6ba895ec2f1c9 --- /dev/null +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.circuit-breaker-queue-processing-fargate-service.ts @@ -0,0 +1,21 @@ +import * as path from 'path'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import { App, Stack } from '@aws-cdk/core'; + +import { QueueProcessingFargateService } from '../../lib'; + +const app = new App(); +const stack = new Stack(app, 'aws-ecs-patterns-queue'); +const vpc = new ec2.Vpc(stack, 'VPC', { + maxAzs: 2, +}); + +new QueueProcessingFargateService(stack, 'QueueProcessingService', { + vpc, + memoryLimitMiB: 512, + circuitBreaker: { rollback: true }, + image: new ecs.AssetImage(path.join(__dirname, '..', 'sqs-reader')), +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts index 1feffcb2e70fd..3a17962c8b230 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts @@ -427,6 +427,58 @@ export = { test.done(); }, + 'setting ALB circuitBreaker works'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'Service', { + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + }, + circuitBreaker: { rollback: true }, + }); + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + DeploymentConfiguration: { + DeploymentCircuitBreaker: { + Enable: true, + Rollback: true, + }, + }, + DeploymentController: { + Type: 'ECS', + }, + })); + test.done(); + }, + + 'setting NLB circuitBreaker works'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ecsPatterns.NetworkLoadBalancedFargateService(stack, 'Service', { + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + }, + circuitBreaker: { rollback: true }, + }); + // THEN + expect(stack).to(haveResourceLike('AWS::ECS::Service', { + DeploymentConfiguration: { + DeploymentCircuitBreaker: { + Enable: true, + Rollback: true, + }, + }, + DeploymentController: { + Type: 'ECS', + }, + })); + test.done(); + }, + 'setting NLB special listener port to create the listener'(test: Test) { // GIVEN const stack = new cdk.Stack(); 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 27671ae22e70d..5207f92629c10 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 @@ -344,9 +344,7 @@ export = { serviceName: 'fargate-test-service', family: 'fargate-task-family', platformVersion: ecs.FargatePlatformVersion.VERSION1_4, - deploymentController: { - type: ecs.DeploymentControllerType.CODE_DEPLOY, - }, + circuitBreaker: { rollback: true }, }); // THEN - QueueWorker is of FARGATE launch type, an SQS queue is created and all optional properties are set. @@ -355,12 +353,16 @@ export = { DeploymentConfiguration: { MinimumHealthyPercent: 60, MaximumPercent: 150, + DeploymentCircuitBreaker: { + Enable: true, + Rollback: true, + }, }, LaunchType: 'FARGATE', ServiceName: 'fargate-test-service', PlatformVersion: ecs.FargatePlatformVersion.VERSION1_4, DeploymentController: { - Type: 'CODE_DEPLOY', + Type: 'ECS', }, })); diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index cc4bd432e7dba..5ad2855c8f642 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -449,10 +449,10 @@ const service = new ecs.Ec2Service(this, 'Service', { /* ... */ }); const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); lb.addListener({ externalPort: 80 }); -lb.addTarget(service.loadBalancerTarget{ +lb.addTarget(service.loadBalancerTarget({ containerName: 'MyContainer', containerPort: 80 -}); +})); ``` There are two higher-level constructs available which include a load balancer for you that can be found in the aws-ecs-patterns module: diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 3ff3f20fd8acd..306b2bced3477 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -387,7 +387,9 @@ export abstract class BaseService extends Resource }, propagateTags: props.propagateTags === PropagatedTagSource.NONE ? undefined : props.propagateTags, enableEcsManagedTags: props.enableECSManagedTags ?? false, - deploymentController: props.deploymentController, + deploymentController: props.circuitBreaker ? { + type: DeploymentControllerType.ECS, + } : props.deploymentController, launchType: launchType, capacityProviderStrategy: props.capacityProviderStrategies, healthCheckGracePeriodSeconds: this.evaluateHealthGracePeriod(props.healthCheckGracePeriod), @@ -984,4 +986,4 @@ function determineContainerNameAndPort(options: DetermineContainerNameAndPortOpt } return {}; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts index 5531b53413f46..cbd43a8d44f67 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts @@ -389,7 +389,7 @@ nodeunitShim({ maxHealthyPercent: 150, minHealthyPercent: 55, deploymentController: { - type: ecs.DeploymentControllerType.CODE_DEPLOY, + type: ecs.DeploymentControllerType.ECS, }, circuitBreaker: { rollback: true }, securityGroup: new ec2.SecurityGroup(stack, 'SecurityGroup1', { @@ -421,7 +421,7 @@ nodeunitShim({ }, }, DeploymentController: { - Type: ecs.DeploymentControllerType.CODE_DEPLOY, + Type: ecs.DeploymentControllerType.ECS, }, DesiredCount: 2, HealthCheckGracePeriodSeconds: 60, @@ -2073,6 +2073,9 @@ nodeunitShim({ Rollback: true, }, }, + DeploymentController: { + Type: ecs.DeploymentControllerType.ECS, + }, })); test.done(); diff --git a/packages/@aws-cdk/aws-efs/README.md b/packages/@aws-cdk/aws-efs/README.md index 002ba355e51eb..444a3a69d7480 100644 --- a/packages/@aws-cdk/aws-efs/README.md +++ b/packages/@aws-cdk/aws-efs/README.md @@ -90,9 +90,9 @@ efs.AccessPoint.fromAccessPointAttributes(this, 'ap', { ⚠️ Notice: When importing an Access Point using `fromAccessPointAttributes()`, you must make sure the mount targets are deployed and their lifecycle state is `available`. Otherwise, you may encounter the following error when deploying: -> EFS file system referenced by access point has +> EFS file system <ARN of efs> referenced by access point <ARN of access point of EFS> has > mount targets created in all availability zones the function will execute in, but not all ->are in the available life cycle state yet. Please wait for them to become available and +> are in the available life cycle state yet. Please wait for them to become available and > try the request again. ### Connecting diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-handlers-vpc.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-handlers-vpc.expected.json index e7e1f394cdc7f..7f612f0788b59 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-handlers-vpc.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-handlers-vpc.expected.json @@ -1115,7 +1115,7 @@ }, "/", { - "Ref": "AssetParameters70396475d85a52e5c6ccad77894979d07433a207ea3c2668b929f3e70ffde081S3BucketFCD070AE" + "Ref": "AssetParameters7b7589d8284d359f531f595891e11720582fb63d0f0051cb08b5bfbcd6d60f60S3BucketE63A0899" }, "/", { @@ -1125,7 +1125,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters70396475d85a52e5c6ccad77894979d07433a207ea3c2668b929f3e70ffde081S3VersionKeyD47BE42B" + "Ref": "AssetParameters7b7589d8284d359f531f595891e11720582fb63d0f0051cb08b5bfbcd6d60f60S3VersionKey91A6BB03" } ] } @@ -1138,7 +1138,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters70396475d85a52e5c6ccad77894979d07433a207ea3c2668b929f3e70ffde081S3VersionKeyD47BE42B" + "Ref": "AssetParameters7b7589d8284d359f531f595891e11720582fb63d0f0051cb08b5bfbcd6d60f60S3VersionKey91A6BB03" } ] } @@ -1196,7 +1196,7 @@ }, "/", { - "Ref": "AssetParameters93bb4c1c6bbced2c1adce602d7643d5475c6e463f4f57f4cd863e064d82a3d82S3Bucket8670C328" + "Ref": "AssetParameters8bdecc7d36d3a82fef0f8acd79a1b98980868636dd7e1e04ba2982b67127ca90S3BucketC889A64C" }, "/", { @@ -1206,7 +1206,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters93bb4c1c6bbced2c1adce602d7643d5475c6e463f4f57f4cd863e064d82a3d82S3VersionKeyCEFB3AF5" + "Ref": "AssetParameters8bdecc7d36d3a82fef0f8acd79a1b98980868636dd7e1e04ba2982b67127ca90S3VersionKeyB6157388" } ] } @@ -1219,7 +1219,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters93bb4c1c6bbced2c1adce602d7643d5475c6e463f4f57f4cd863e064d82a3d82S3VersionKeyCEFB3AF5" + "Ref": "AssetParameters8bdecc7d36d3a82fef0f8acd79a1b98980868636dd7e1e04ba2982b67127ca90S3VersionKeyB6157388" } ] } @@ -1389,29 +1389,29 @@ "Type": "String", "Description": "Artifact hash for asset \"844c1a4b13479b359ea0e607dccb4a04b73e22cf88cf9b64feed2c5f0de213c0\"" }, - "AssetParameters70396475d85a52e5c6ccad77894979d07433a207ea3c2668b929f3e70ffde081S3BucketFCD070AE": { + "AssetParameters7b7589d8284d359f531f595891e11720582fb63d0f0051cb08b5bfbcd6d60f60S3BucketE63A0899": { "Type": "String", - "Description": "S3 bucket for asset \"70396475d85a52e5c6ccad77894979d07433a207ea3c2668b929f3e70ffde081\"" + "Description": "S3 bucket for asset \"7b7589d8284d359f531f595891e11720582fb63d0f0051cb08b5bfbcd6d60f60\"" }, - "AssetParameters70396475d85a52e5c6ccad77894979d07433a207ea3c2668b929f3e70ffde081S3VersionKeyD47BE42B": { + "AssetParameters7b7589d8284d359f531f595891e11720582fb63d0f0051cb08b5bfbcd6d60f60S3VersionKey91A6BB03": { "Type": "String", - "Description": "S3 key for asset version \"70396475d85a52e5c6ccad77894979d07433a207ea3c2668b929f3e70ffde081\"" + "Description": "S3 key for asset version \"7b7589d8284d359f531f595891e11720582fb63d0f0051cb08b5bfbcd6d60f60\"" }, - "AssetParameters70396475d85a52e5c6ccad77894979d07433a207ea3c2668b929f3e70ffde081ArtifactHashF56FF52E": { + "AssetParameters7b7589d8284d359f531f595891e11720582fb63d0f0051cb08b5bfbcd6d60f60ArtifactHashEB6B332A": { "Type": "String", - "Description": "Artifact hash for asset \"70396475d85a52e5c6ccad77894979d07433a207ea3c2668b929f3e70ffde081\"" + "Description": "Artifact hash for asset \"7b7589d8284d359f531f595891e11720582fb63d0f0051cb08b5bfbcd6d60f60\"" }, - "AssetParameters93bb4c1c6bbced2c1adce602d7643d5475c6e463f4f57f4cd863e064d82a3d82S3Bucket8670C328": { + "AssetParameters8bdecc7d36d3a82fef0f8acd79a1b98980868636dd7e1e04ba2982b67127ca90S3BucketC889A64C": { "Type": "String", - "Description": "S3 bucket for asset \"93bb4c1c6bbced2c1adce602d7643d5475c6e463f4f57f4cd863e064d82a3d82\"" + "Description": "S3 bucket for asset \"8bdecc7d36d3a82fef0f8acd79a1b98980868636dd7e1e04ba2982b67127ca90\"" }, - "AssetParameters93bb4c1c6bbced2c1adce602d7643d5475c6e463f4f57f4cd863e064d82a3d82S3VersionKeyCEFB3AF5": { + "AssetParameters8bdecc7d36d3a82fef0f8acd79a1b98980868636dd7e1e04ba2982b67127ca90S3VersionKeyB6157388": { "Type": "String", - "Description": "S3 key for asset version \"93bb4c1c6bbced2c1adce602d7643d5475c6e463f4f57f4cd863e064d82a3d82\"" + "Description": "S3 key for asset version \"8bdecc7d36d3a82fef0f8acd79a1b98980868636dd7e1e04ba2982b67127ca90\"" }, - "AssetParameters93bb4c1c6bbced2c1adce602d7643d5475c6e463f4f57f4cd863e064d82a3d82ArtifactHashBEC324DA": { + "AssetParameters8bdecc7d36d3a82fef0f8acd79a1b98980868636dd7e1e04ba2982b67127ca90ArtifactHash8A7D59E8": { "Type": "String", - "Description": "Artifact hash for asset \"93bb4c1c6bbced2c1adce602d7643d5475c6e463f4f57f4cd863e064d82a3d82\"" + "Description": "Artifact hash for asset \"8bdecc7d36d3a82fef0f8acd79a1b98980868636dd7e1e04ba2982b67127ca90\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-private-endpoint.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-private-endpoint.expected.json index acbe47374bead..8f59f96ca7a6b 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-private-endpoint.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster-private-endpoint.expected.json @@ -1048,7 +1048,7 @@ }, "/", { - "Ref": "AssetParameters75667ab2bbef2c8efc57fb73bf352f345af1d471fb09cb11f5b7bc27d009b609S3BucketA8C94679" + "Ref": "AssetParametersf2b553bd53fbb997e6f23206daa298a3f8bfa7c9805c8c7c99155b4acc2d0cd6S3BucketFA2AD206" }, "/", { @@ -1058,7 +1058,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters75667ab2bbef2c8efc57fb73bf352f345af1d471fb09cb11f5b7bc27d009b609S3VersionKey3777DB64" + "Ref": "AssetParametersf2b553bd53fbb997e6f23206daa298a3f8bfa7c9805c8c7c99155b4acc2d0cd6S3VersionKeyD5C722C7" } ] } @@ -1071,7 +1071,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters75667ab2bbef2c8efc57fb73bf352f345af1d471fb09cb11f5b7bc27d009b609S3VersionKey3777DB64" + "Ref": "AssetParametersf2b553bd53fbb997e6f23206daa298a3f8bfa7c9805c8c7c99155b4acc2d0cd6S3VersionKeyD5C722C7" } ] } @@ -1117,7 +1117,7 @@ }, "/", { - "Ref": "AssetParameters8d4ffe9194a1ca97e2e9377049ddbeb3adbdaa984c8a90c08a5d8d6b328bdf7cS3BucketFEA5F85E" + "Ref": "AssetParameters5988c5261a097d0fc0f9ce78bb67db66f6ddf5c934fc58fa695dc27acaf6e69fS3Bucket5150D79C" }, "/", { @@ -1127,7 +1127,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters8d4ffe9194a1ca97e2e9377049ddbeb3adbdaa984c8a90c08a5d8d6b328bdf7cS3VersionKey226CF52C" + "Ref": "AssetParameters5988c5261a097d0fc0f9ce78bb67db66f6ddf5c934fc58fa695dc27acaf6e69fS3VersionKey7F05152C" } ] } @@ -1140,7 +1140,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters8d4ffe9194a1ca97e2e9377049ddbeb3adbdaa984c8a90c08a5d8d6b328bdf7cS3VersionKey226CF52C" + "Ref": "AssetParameters5988c5261a097d0fc0f9ce78bb67db66f6ddf5c934fc58fa695dc27acaf6e69fS3VersionKey7F05152C" } ] } @@ -1310,29 +1310,29 @@ "Type": "String", "Description": "Artifact hash for asset \"844c1a4b13479b359ea0e607dccb4a04b73e22cf88cf9b64feed2c5f0de213c0\"" }, - "AssetParameters75667ab2bbef2c8efc57fb73bf352f345af1d471fb09cb11f5b7bc27d009b609S3BucketA8C94679": { + "AssetParametersf2b553bd53fbb997e6f23206daa298a3f8bfa7c9805c8c7c99155b4acc2d0cd6S3BucketFA2AD206": { "Type": "String", - "Description": "S3 bucket for asset \"75667ab2bbef2c8efc57fb73bf352f345af1d471fb09cb11f5b7bc27d009b609\"" + "Description": "S3 bucket for asset \"f2b553bd53fbb997e6f23206daa298a3f8bfa7c9805c8c7c99155b4acc2d0cd6\"" }, - "AssetParameters75667ab2bbef2c8efc57fb73bf352f345af1d471fb09cb11f5b7bc27d009b609S3VersionKey3777DB64": { + "AssetParametersf2b553bd53fbb997e6f23206daa298a3f8bfa7c9805c8c7c99155b4acc2d0cd6S3VersionKeyD5C722C7": { "Type": "String", - "Description": "S3 key for asset version \"75667ab2bbef2c8efc57fb73bf352f345af1d471fb09cb11f5b7bc27d009b609\"" + "Description": "S3 key for asset version \"f2b553bd53fbb997e6f23206daa298a3f8bfa7c9805c8c7c99155b4acc2d0cd6\"" }, - "AssetParameters75667ab2bbef2c8efc57fb73bf352f345af1d471fb09cb11f5b7bc27d009b609ArtifactHash14CC8C95": { + "AssetParametersf2b553bd53fbb997e6f23206daa298a3f8bfa7c9805c8c7c99155b4acc2d0cd6ArtifactHashD053AD55": { "Type": "String", - "Description": "Artifact hash for asset \"75667ab2bbef2c8efc57fb73bf352f345af1d471fb09cb11f5b7bc27d009b609\"" + "Description": "Artifact hash for asset \"f2b553bd53fbb997e6f23206daa298a3f8bfa7c9805c8c7c99155b4acc2d0cd6\"" }, - "AssetParameters8d4ffe9194a1ca97e2e9377049ddbeb3adbdaa984c8a90c08a5d8d6b328bdf7cS3BucketFEA5F85E": { + "AssetParameters5988c5261a097d0fc0f9ce78bb67db66f6ddf5c934fc58fa695dc27acaf6e69fS3Bucket5150D79C": { "Type": "String", - "Description": "S3 bucket for asset \"8d4ffe9194a1ca97e2e9377049ddbeb3adbdaa984c8a90c08a5d8d6b328bdf7c\"" + "Description": "S3 bucket for asset \"5988c5261a097d0fc0f9ce78bb67db66f6ddf5c934fc58fa695dc27acaf6e69f\"" }, - "AssetParameters8d4ffe9194a1ca97e2e9377049ddbeb3adbdaa984c8a90c08a5d8d6b328bdf7cS3VersionKey226CF52C": { + "AssetParameters5988c5261a097d0fc0f9ce78bb67db66f6ddf5c934fc58fa695dc27acaf6e69fS3VersionKey7F05152C": { "Type": "String", - "Description": "S3 key for asset version \"8d4ffe9194a1ca97e2e9377049ddbeb3adbdaa984c8a90c08a5d8d6b328bdf7c\"" + "Description": "S3 key for asset version \"5988c5261a097d0fc0f9ce78bb67db66f6ddf5c934fc58fa695dc27acaf6e69f\"" }, - "AssetParameters8d4ffe9194a1ca97e2e9377049ddbeb3adbdaa984c8a90c08a5d8d6b328bdf7cArtifactHashA6BF0EB3": { + "AssetParameters5988c5261a097d0fc0f9ce78bb67db66f6ddf5c934fc58fa695dc27acaf6e69fArtifactHashEE0F431C": { "Type": "String", - "Description": "Artifact hash for asset \"8d4ffe9194a1ca97e2e9377049ddbeb3adbdaa984c8a90c08a5d8d6b328bdf7c\"" + "Description": "Artifact hash for asset \"5988c5261a097d0fc0f9ce78bb67db66f6ddf5c934fc58fa695dc27acaf6e69f\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index c157211d73f87..1ea62568197d4 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -3810,7 +3810,7 @@ }, "/", { - "Ref": "AssetParameters264acf17cbf0c643f47bec1f4dbaed805e3bd1bad3f018c093d16fb936227daaS3Bucket862E8D6F" + "Ref": "AssetParameters5598bd5ce38da10f7a9c6f8e54d4f50d7c0befd5309540ab64d64985236f2ef9S3BucketC3BFBE73" }, "/", { @@ -3820,7 +3820,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters264acf17cbf0c643f47bec1f4dbaed805e3bd1bad3f018c093d16fb936227daaS3VersionKey9466BE3D" + "Ref": "AssetParameters5598bd5ce38da10f7a9c6f8e54d4f50d7c0befd5309540ab64d64985236f2ef9S3VersionKeyFA8225D5" } ] } @@ -3833,7 +3833,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters264acf17cbf0c643f47bec1f4dbaed805e3bd1bad3f018c093d16fb936227daaS3VersionKey9466BE3D" + "Ref": "AssetParameters5598bd5ce38da10f7a9c6f8e54d4f50d7c0befd5309540ab64d64985236f2ef9S3VersionKeyFA8225D5" } ] } @@ -3879,7 +3879,7 @@ }, "/", { - "Ref": "AssetParameterse00474136b86298d67c0ad626f1dafe622c51168233097531c5ba6791d43aba3S3Bucket5829AD66" + "Ref": "AssetParameterse334ff007b5126b62d66d4baac94004f4281dc2b0b0c4685ec93e330cb59e921S3Bucket706500B1" }, "/", { @@ -3889,7 +3889,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse00474136b86298d67c0ad626f1dafe622c51168233097531c5ba6791d43aba3S3VersionKey44FDB4A8" + "Ref": "AssetParameterse334ff007b5126b62d66d4baac94004f4281dc2b0b0c4685ec93e330cb59e921S3VersionKeyAB3509E4" } ] } @@ -3902,7 +3902,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterse00474136b86298d67c0ad626f1dafe622c51168233097531c5ba6791d43aba3S3VersionKey44FDB4A8" + "Ref": "AssetParameterse334ff007b5126b62d66d4baac94004f4281dc2b0b0c4685ec93e330cb59e921S3VersionKeyAB3509E4" } ] } @@ -4068,7 +4068,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0deS3Bucket14156880" + "Ref": "AssetParametersb7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4S3BucketF7BC1777" }, "S3Key": { "Fn::Join": [ @@ -4081,7 +4081,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0deS3VersionKey5225BCA4" + "Ref": "AssetParametersb7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4S3VersionKey1C340B30" } ] } @@ -4094,7 +4094,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0deS3VersionKey5225BCA4" + "Ref": "AssetParametersb7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4S3VersionKey1C340B30" } ] } @@ -4469,7 +4469,7 @@ } }, "Handler": "framework.onEvent", - "Runtime": "nodejs10.x", + "Runtime": "nodejs14.x", "Timeout": 900 }, "DependsOn": [ @@ -4662,17 +4662,17 @@ "Type": "String", "Description": "Artifact hash for asset \"844c1a4b13479b359ea0e607dccb4a04b73e22cf88cf9b64feed2c5f0de213c0\"" }, - "AssetParametersb075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0deS3Bucket14156880": { + "AssetParametersb7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4S3BucketF7BC1777": { "Type": "String", - "Description": "S3 bucket for asset \"b075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0de\"" + "Description": "S3 bucket for asset \"b7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4\"" }, - "AssetParametersb075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0deS3VersionKey5225BCA4": { + "AssetParametersb7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4S3VersionKey1C340B30": { "Type": "String", - "Description": "S3 key for asset version \"b075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0de\"" + "Description": "S3 key for asset version \"b7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4\"" }, - "AssetParametersb075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0deArtifactHashC509349A": { + "AssetParametersb7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4ArtifactHashD6EA1BC7": { "Type": "String", - "Description": "Artifact hash for asset \"b075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0de\"" + "Description": "Artifact hash for asset \"b7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4\"" }, "AssetParameters952bd1c03e8201c4c1c67d6de0f3fdaaf88fda05f89a1232c3f6364343cd5344S3Bucket055DC235": { "Type": "String", @@ -4698,29 +4698,29 @@ "Type": "String", "Description": "Artifact hash for asset \"5f49893093e1ad14831626016699156d48da5f0890f19eb930bc3c46cf5f636d\"" }, - "AssetParameters264acf17cbf0c643f47bec1f4dbaed805e3bd1bad3f018c093d16fb936227daaS3Bucket862E8D6F": { + "AssetParameters5598bd5ce38da10f7a9c6f8e54d4f50d7c0befd5309540ab64d64985236f2ef9S3BucketC3BFBE73": { "Type": "String", - "Description": "S3 bucket for asset \"264acf17cbf0c643f47bec1f4dbaed805e3bd1bad3f018c093d16fb936227daa\"" + "Description": "S3 bucket for asset \"5598bd5ce38da10f7a9c6f8e54d4f50d7c0befd5309540ab64d64985236f2ef9\"" }, - "AssetParameters264acf17cbf0c643f47bec1f4dbaed805e3bd1bad3f018c093d16fb936227daaS3VersionKey9466BE3D": { + "AssetParameters5598bd5ce38da10f7a9c6f8e54d4f50d7c0befd5309540ab64d64985236f2ef9S3VersionKeyFA8225D5": { "Type": "String", - "Description": "S3 key for asset version \"264acf17cbf0c643f47bec1f4dbaed805e3bd1bad3f018c093d16fb936227daa\"" + "Description": "S3 key for asset version \"5598bd5ce38da10f7a9c6f8e54d4f50d7c0befd5309540ab64d64985236f2ef9\"" }, - "AssetParameters264acf17cbf0c643f47bec1f4dbaed805e3bd1bad3f018c093d16fb936227daaArtifactHashC5F5158C": { + "AssetParameters5598bd5ce38da10f7a9c6f8e54d4f50d7c0befd5309540ab64d64985236f2ef9ArtifactHash8F06BD93": { "Type": "String", - "Description": "Artifact hash for asset \"264acf17cbf0c643f47bec1f4dbaed805e3bd1bad3f018c093d16fb936227daa\"" + "Description": "Artifact hash for asset \"5598bd5ce38da10f7a9c6f8e54d4f50d7c0befd5309540ab64d64985236f2ef9\"" }, - "AssetParameterse00474136b86298d67c0ad626f1dafe622c51168233097531c5ba6791d43aba3S3Bucket5829AD66": { + "AssetParameterse334ff007b5126b62d66d4baac94004f4281dc2b0b0c4685ec93e330cb59e921S3Bucket706500B1": { "Type": "String", - "Description": "S3 bucket for asset \"e00474136b86298d67c0ad626f1dafe622c51168233097531c5ba6791d43aba3\"" + "Description": "S3 bucket for asset \"e334ff007b5126b62d66d4baac94004f4281dc2b0b0c4685ec93e330cb59e921\"" }, - "AssetParameterse00474136b86298d67c0ad626f1dafe622c51168233097531c5ba6791d43aba3S3VersionKey44FDB4A8": { + "AssetParameterse334ff007b5126b62d66d4baac94004f4281dc2b0b0c4685ec93e330cb59e921S3VersionKeyAB3509E4": { "Type": "String", - "Description": "S3 key for asset version \"e00474136b86298d67c0ad626f1dafe622c51168233097531c5ba6791d43aba3\"" + "Description": "S3 key for asset version \"e334ff007b5126b62d66d4baac94004f4281dc2b0b0c4685ec93e330cb59e921\"" }, - "AssetParameterse00474136b86298d67c0ad626f1dafe622c51168233097531c5ba6791d43aba3ArtifactHashB6A3908A": { + "AssetParameterse334ff007b5126b62d66d4baac94004f4281dc2b0b0c4685ec93e330cb59e921ArtifactHash31BE978F": { "Type": "String", - "Description": "Artifact hash for asset \"e00474136b86298d67c0ad626f1dafe622c51168233097531c5ba6791d43aba3\"" + "Description": "Artifact hash for asset \"e334ff007b5126b62d66d4baac94004f4281dc2b0b0c4685ec93e330cb59e921\"" }, "SsmParameterValueawsserviceeksoptimizedami119amazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { "Type": "AWS::SSM::Parameter::Value", diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-oidc-provider.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-oidc-provider.expected.json index b6a2cad449f9c..0f0a5c61cdf67 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-oidc-provider.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-oidc-provider.expected.json @@ -79,7 +79,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersb075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0deS3Bucket14156880" + "Ref": "AssetParametersb7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4S3BucketF7BC1777" }, "S3Key": { "Fn::Join": [ @@ -92,7 +92,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0deS3VersionKey5225BCA4" + "Ref": "AssetParametersb7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4S3VersionKey1C340B30" } ] } @@ -105,7 +105,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersb075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0deS3VersionKey5225BCA4" + "Ref": "AssetParametersb7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4S3VersionKey1C340B30" } ] } @@ -132,17 +132,17 @@ } }, "Parameters": { - "AssetParametersb075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0deS3Bucket14156880": { + "AssetParametersb7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4S3BucketF7BC1777": { "Type": "String", - "Description": "S3 bucket for asset \"b075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0de\"" + "Description": "S3 bucket for asset \"b7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4\"" }, - "AssetParametersb075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0deS3VersionKey5225BCA4": { + "AssetParametersb7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4S3VersionKey1C340B30": { "Type": "String", - "Description": "S3 key for asset version \"b075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0de\"" + "Description": "S3 key for asset version \"b7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4\"" }, - "AssetParametersb075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0deArtifactHashC509349A": { + "AssetParametersb7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4ArtifactHashD6EA1BC7": { "Type": "String", - "Description": "Artifact hash for asset \"b075459e6bf309093fbd4b9a9e576a5f172b91c14d84eedb0f069566f6abb0de\"" + "Description": "Artifact hash for asset \"b7d38dc0eeb2c5d024919020e09d2590b68559eab4a5264c3b1aa7a429d1edd4\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/integ.fargate-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.fargate-cluster.expected.json index 441cac51ffd69..c8d1c3a68bdf9 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.fargate-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.fargate-cluster.expected.json @@ -1125,7 +1125,7 @@ }, "/", { - "Ref": "AssetParameters3d252d05ccf0ae2934dd20707e8a709b466b2b8ea00c04ee8735667f90b17ac1S3Bucket01B07207" + "Ref": "AssetParameters8f90971131eea6a356f7cfb4997f4b70bb577cf34d007550f5e28c342f6c47f7S3Bucket3C7D361B" }, "/", { @@ -1135,7 +1135,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3d252d05ccf0ae2934dd20707e8a709b466b2b8ea00c04ee8735667f90b17ac1S3VersionKey3EEF52BA" + "Ref": "AssetParameters8f90971131eea6a356f7cfb4997f4b70bb577cf34d007550f5e28c342f6c47f7S3VersionKey455B40AC" } ] } @@ -1148,7 +1148,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3d252d05ccf0ae2934dd20707e8a709b466b2b8ea00c04ee8735667f90b17ac1S3VersionKey3EEF52BA" + "Ref": "AssetParameters8f90971131eea6a356f7cfb4997f4b70bb577cf34d007550f5e28c342f6c47f7S3VersionKey455B40AC" } ] } @@ -1194,7 +1194,7 @@ }, "/", { - "Ref": "AssetParametersedcd50bb250cf7cf664a737720e623ad66ada9b9e609205686c476e516cf2f23S3Bucket586F6135" + "Ref": "AssetParameters66581105512a058e4f8b54fb8be68bf58cd32bfa5e87be3574d5e53eab943e49S3Bucket860C7E3D" }, "/", { @@ -1204,7 +1204,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersedcd50bb250cf7cf664a737720e623ad66ada9b9e609205686c476e516cf2f23S3VersionKey6EFBFC1D" + "Ref": "AssetParameters66581105512a058e4f8b54fb8be68bf58cd32bfa5e87be3574d5e53eab943e49S3VersionKey11F1F533" } ] } @@ -1217,7 +1217,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersedcd50bb250cf7cf664a737720e623ad66ada9b9e609205686c476e516cf2f23S3VersionKey6EFBFC1D" + "Ref": "AssetParameters66581105512a058e4f8b54fb8be68bf58cd32bfa5e87be3574d5e53eab943e49S3VersionKey11F1F533" } ] } @@ -1387,29 +1387,29 @@ "Type": "String", "Description": "Artifact hash for asset \"844c1a4b13479b359ea0e607dccb4a04b73e22cf88cf9b64feed2c5f0de213c0\"" }, - "AssetParameters3d252d05ccf0ae2934dd20707e8a709b466b2b8ea00c04ee8735667f90b17ac1S3Bucket01B07207": { + "AssetParameters8f90971131eea6a356f7cfb4997f4b70bb577cf34d007550f5e28c342f6c47f7S3Bucket3C7D361B": { "Type": "String", - "Description": "S3 bucket for asset \"3d252d05ccf0ae2934dd20707e8a709b466b2b8ea00c04ee8735667f90b17ac1\"" + "Description": "S3 bucket for asset \"8f90971131eea6a356f7cfb4997f4b70bb577cf34d007550f5e28c342f6c47f7\"" }, - "AssetParameters3d252d05ccf0ae2934dd20707e8a709b466b2b8ea00c04ee8735667f90b17ac1S3VersionKey3EEF52BA": { + "AssetParameters8f90971131eea6a356f7cfb4997f4b70bb577cf34d007550f5e28c342f6c47f7S3VersionKey455B40AC": { "Type": "String", - "Description": "S3 key for asset version \"3d252d05ccf0ae2934dd20707e8a709b466b2b8ea00c04ee8735667f90b17ac1\"" + "Description": "S3 key for asset version \"8f90971131eea6a356f7cfb4997f4b70bb577cf34d007550f5e28c342f6c47f7\"" }, - "AssetParameters3d252d05ccf0ae2934dd20707e8a709b466b2b8ea00c04ee8735667f90b17ac1ArtifactHash812ED4D5": { + "AssetParameters8f90971131eea6a356f7cfb4997f4b70bb577cf34d007550f5e28c342f6c47f7ArtifactHashA258B8DC": { "Type": "String", - "Description": "Artifact hash for asset \"3d252d05ccf0ae2934dd20707e8a709b466b2b8ea00c04ee8735667f90b17ac1\"" + "Description": "Artifact hash for asset \"8f90971131eea6a356f7cfb4997f4b70bb577cf34d007550f5e28c342f6c47f7\"" }, - "AssetParametersedcd50bb250cf7cf664a737720e623ad66ada9b9e609205686c476e516cf2f23S3Bucket586F6135": { + "AssetParameters66581105512a058e4f8b54fb8be68bf58cd32bfa5e87be3574d5e53eab943e49S3Bucket860C7E3D": { "Type": "String", - "Description": "S3 bucket for asset \"edcd50bb250cf7cf664a737720e623ad66ada9b9e609205686c476e516cf2f23\"" + "Description": "S3 bucket for asset \"66581105512a058e4f8b54fb8be68bf58cd32bfa5e87be3574d5e53eab943e49\"" }, - "AssetParametersedcd50bb250cf7cf664a737720e623ad66ada9b9e609205686c476e516cf2f23S3VersionKey6EFBFC1D": { + "AssetParameters66581105512a058e4f8b54fb8be68bf58cd32bfa5e87be3574d5e53eab943e49S3VersionKey11F1F533": { "Type": "String", - "Description": "S3 key for asset version \"edcd50bb250cf7cf664a737720e623ad66ada9b9e609205686c476e516cf2f23\"" + "Description": "S3 key for asset version \"66581105512a058e4f8b54fb8be68bf58cd32bfa5e87be3574d5e53eab943e49\"" }, - "AssetParametersedcd50bb250cf7cf664a737720e623ad66ada9b9e609205686c476e516cf2f23ArtifactHashB34EB8FE": { + "AssetParameters66581105512a058e4f8b54fb8be68bf58cd32bfa5e87be3574d5e53eab943e49ArtifactHash660928E1": { "Type": "String", - "Description": "Artifact hash for asset \"edcd50bb250cf7cf664a737720e623ad66ada9b9e609205686c476e516cf2f23\"" + "Description": "Artifact hash for asset \"66581105512a058e4f8b54fb8be68bf58cd32bfa5e87be3574d5e53eab943e49\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index e0f3b1f4e5ea5..b04d39e1862d7 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -273,6 +273,20 @@ const tg2 = new elbv2.ApplicationTargetGroup(stack, 'TG2', { For more information see: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/sticky-sessions.html#application-based-stickiness +### Setting the target group protocol version + +By default, Application Load Balancers send requests to targets using HTTP/1.1. You can use the [protocol version](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html#target-group-protocol-version) to send requests to targets using HTTP/2 or gRPC. + +```ts +const tg = new elbv2.ApplicationTargetGroup(stack, 'TG', { + targetType: elbv2.TargetType.IP, + port: 50051, + protocol: elbv2.ApplicationProtocol.HTTP, + protocolVersion: elbv2.ApplicationProtocolVersion.GRPC, + vpc, +}); +``` + ## Using Lambda Targets To use a Lambda Function as a target, use the integration class in the diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts index ad3cfadf2f3c8..14e0855e0988e 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-target-group.ts @@ -7,7 +7,7 @@ import { BaseTargetGroupProps, ITargetGroup, loadBalancerNameFromListenerArn, LoadBalancerTargetProps, TargetGroupAttributes, TargetGroupBase, TargetGroupImportProps, } from '../shared/base-target-group'; -import { ApplicationProtocol, Protocol, TargetType } from '../shared/enums'; +import { ApplicationProtocol, ApplicationProtocolVersion, Protocol, TargetType } from '../shared/enums'; import { ImportedTargetGroupBase } from '../shared/imported'; import { determineProtocolAndPort } from '../shared/util'; import { IApplicationListener } from './application-listener'; @@ -24,6 +24,13 @@ export interface ApplicationTargetGroupProps extends BaseTargetGroupProps { */ readonly protocol?: ApplicationProtocol; + /** + * The protocol version to use + * + * @default ApplicationProtocolVersion.HTTP1 + */ + readonly protocolVersion?: ApplicationProtocolVersion; + /** * The port on which the listener listens for requests. * @@ -106,8 +113,10 @@ export class ApplicationTargetGroup extends TargetGroupBase implements IApplicat constructor(scope: Construct, id: string, props: ApplicationTargetGroupProps = {}) { const [protocol, port] = determineProtocolAndPort(props.protocol, props.port); + const { protocolVersion } = props; super(scope, id, { ...props }, { protocol, + protocolVersion, port, }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts index be25c49b96513..8c5f183bb2cc2 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/enums.ts @@ -63,6 +63,26 @@ export enum ApplicationProtocol { HTTPS = 'HTTPS' } +/** + * Load balancing protocol version for application load balancers + */ +export enum ApplicationProtocolVersion { + /** + * GRPC + */ + GRPC = 'GRPC', + + /** + * HTTP1 + */ + HTTP1 = 'HTTP1', + + /** + * HTTP2 + */ + HTTP2 = 'HTTP2', +} + /** * Elastic Load Balancing provides the following security policies for Application Load Balancers * diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts index 77858e9c21af4..ea028b543096f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts @@ -156,6 +156,24 @@ describe('tests', () => { }); }); + test('Can set a protocol version', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // WHEN + new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + vpc, + protocolVersion: elbv2.ApplicationProtocolVersion.GRPC, + }); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + ProtocolVersion: 'GRPC', + }); + }); + test('Bad stickiness cookie names', () => { // GIVEN const app = new cdk.App(); diff --git a/packages/@aws-cdk/aws-events/README.md b/packages/@aws-cdk/aws-events/README.md index bb0e434837131..4bf4c2b390dbe 100644 --- a/packages/@aws-cdk/aws-events/README.md +++ b/packages/@aws-cdk/aws-events/README.md @@ -15,7 +15,7 @@ Amazon EventBridge delivers a near real-time stream of system events that describe changes in AWS resources. For example, an AWS CodePipeline emits the [State Change](https://docs.aws.amazon.com/eventbridge/latest/userguide/event-types.html#codepipeline-event-type) -event when the pipeline changes it's state. +event when the pipeline changes its state. * __Events__: An event indicates a change in your AWS environment. AWS resources can generate events when their state changes. For example, Amazon EC2 diff --git a/packages/@aws-cdk/aws-iam/lib/policy-statement.ts b/packages/@aws-cdk/aws-iam/lib/policy-statement.ts index ce817a58e508e..78a588760c9d6 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-statement.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-statement.ts @@ -64,7 +64,8 @@ export class PolicyStatement { constructor(props: PolicyStatementProps = {}) { // Validate actions for (const action of [...props.actions || [], ...props.notActions || []]) { - if (!/^(\*|[a-zA-Z0-9-]+:[a-zA-Z0-9*]+)$/.test(action)) { + + if (!/^(\*|[a-zA-Z0-9-]+:[a-zA-Z0-9*]+)$/.test(action) && !cdk.Token.isUnresolved(action)) { throw new Error(`Action '${action}' is invalid. An action string consists of a service namespace, a colon, and the name of an action. Action names can include wildcards.`); } } diff --git a/packages/@aws-cdk/aws-iam/test/policy-document.test.ts b/packages/@aws-cdk/aws-iam/test/policy-document.test.ts index d8a9b1337c21c..bd3bd6fd31aa3 100644 --- a/packages/@aws-cdk/aws-iam/test/policy-document.test.ts +++ b/packages/@aws-cdk/aws-iam/test/policy-document.test.ts @@ -102,6 +102,19 @@ describe('IAM policy document', () => { }).toThrow(/Action 'in:val:id' is invalid/); }); + // https://github.com/aws/aws-cdk/issues/13479 + test('Does not validate unresolved tokens', () => { + const stack = new Stack(); + const perm = new PolicyStatement({ + actions: [`${Lazy.string({ produce: () => 'sqs:sendMessage' })}`], + }); + + expect(stack.resolve(perm.toStatementJson())).toEqual({ + Effect: 'Allow', + Action: 'sqs:sendMessage', + }); + }); + test('Cannot combine Resources and NotResources', () => { expect(() => { new PolicyStatement({ diff --git a/packages/@aws-cdk/aws-lambda-event-sources/README.md b/packages/@aws-cdk/aws-lambda-event-sources/README.md index 5e1578d9aefd4..eb4f206c8f3c9 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/README.md +++ b/packages/@aws-cdk/aws-lambda-event-sources/README.md @@ -207,6 +207,67 @@ myFunction.addEventSource(new KinesisEventSource(stream, { })); ``` +## Kafka + +You can write Lambda functions to process data either from [Amazon MSK](https://docs.aws.amazon.com/lambda/latest/dg/with-msk.html) or a [self managed Kafka](https://docs.aws.amazon.com/lambda/latest/dg/kafka-smaa.html) cluster. + +The following code sets up Amazon MSK as an event source for a lambda function. Credentials will need to be configured to access the +MSK cluster, as described in [Username/Password authentication](https://docs.aws.amazon.com/msk/latest/developerguide/msk-password.html). + +```ts +import * as lambda from '@aws-cdk/aws-lambda'; +import * as msk from '@aws-cdk/aws-lambda'; +import { Secret } from '@aws-cdk/aws-secretmanager'; +import { ManagedKafkaEventSource } from '@aws-cdk/aws-lambda-event-sources'; + +// Your MSK cluster +const cluster = msk.Cluster.fromClusterArn(this, 'Cluster', + 'arn:aws:kafka:us-east-1:0123456789019:cluster/SalesCluster/abcd1234-abcd-cafe-abab-9876543210ab-4'); + +// The Kafka topic you want to subscribe to +const topic = 'some-cool-topic' + +// The secret that allows access to your MSK cluster +// You still have to make sure that it is associated with your cluster as described in the documentation +const secret = new Secret(this, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + +myFunction.addEventSource(new ManagedKafkaEventSource({ + cluster: cluster, + topic: topic, + secret: secret, + batchSize: 100, // default + startingPosition: lambda.StartingPosition.TRIM_HORIZON +})); +``` + +The following code sets up a self managed Kafka cluster as an event source. Username and password based authentication +will need to be set up as described in [Managing access and permissions](https://docs.aws.amazon.com/lambda/latest/dg/smaa-permissions.html#smaa-permissions-add-secret). + +```ts +import * as lambda from '@aws-cdk/aws-lambda'; +import { Secret } from '@aws-cdk/aws-secretmanager'; +import { SelfManagedKafkaEventSource } from '@aws-cdk/aws-lambda-event-sources'; + +// The list of Kafka brokers +const bootstrapServers = ['kafka-broker:9092'] + +// The Kafka topic you want to subscribe to +const topic = 'some-cool-topic' + +// The secret that allows access to your self hosted Kafka cluster +const secret = new Secret(this, 'Secret', { ... }); + +myFunction.addEventSource(new SelfManagedKafkaEventSource({ + bootstrapServers: bootstrapServers, + topic: topic, + secret: secret, + batchSize: 100, // default + startingPosition: lambda.StartingPosition.TRIM_HORIZON +})); +``` + +If your self managed Kafka cluster is only reachable via VPC also configure `vpc` `vpcSubnets` and `securityGroup`. + ## Roadmap Eventually, this module will support all the event sources described under diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/index.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/index.ts index 19253a743cae8..555137e51afbf 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/index.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/index.ts @@ -1,5 +1,6 @@ export * from './api'; export * from './dynamodb'; +export * from './kafka'; export * from './kinesis'; export * from './s3'; export * from './sns'; diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts new file mode 100644 index 0000000000000..56e6b2a64ce26 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts @@ -0,0 +1,191 @@ +import * as crypto from 'crypto'; +import { ISecurityGroup, IVpc, SubnetSelection } from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as msk from '@aws-cdk/aws-msk'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { StreamEventSource, StreamEventSourceProps } from './stream'; + +/** + * Properties for a Kafka event source + */ +export interface KafkaEventSourceProps extends StreamEventSourceProps { + /** + * the Kafka topic to subscribe to + */ + readonly topic: string, + /** + * the secret with the Kafka credentials, see https://docs.aws.amazon.com/msk/latest/developerguide/msk-password.html for details + */ + readonly secret: secretsmanager.ISecret +} + +/** + * Properties for a MSK event source + */ +export interface ManagedKafkaEventSourceProps extends KafkaEventSourceProps { + /** + * an MSK cluster construct + */ + readonly cluster: msk.ICluster +} + +/** + * The authentication method to use with SelfManagedKafkaEventSource + */ +export enum AuthenticationMethod { + /** + * SASL_SCRAM_512_AUTH authentication method for your Kafka cluster + */ + SASL_SCRAM_512_AUTH = 'SASL_SCRAM_512_AUTH', + /** + * SASL_SCRAM_256_AUTH authentication method for your Kafka cluster + */ + SASL_SCRAM_256_AUTH = 'SASL_SCRAM_256_AUTH', +} + +/** + * Properties for a self managed Kafka cluster event source. + * If your Kafka cluster is only reachable via VPC make sure to configure it. + */ +export interface SelfManagedKafkaEventSourceProps extends KafkaEventSourceProps { + /** + * The list of host and port pairs that are the addresses of the Kafka brokers in a "bootstrap" Kafka cluster that + * a Kafka client connects to initially to bootstrap itself. They are in the format `abc.xyz.com:xxxx`. + */ + readonly bootstrapServers: string[] + + /** + * If your Kafka brokers are only reachable via VPC provide the VPC here + * + * @default none + */ + readonly vpc?: IVpc; + + /** + * If your Kafka brokers are only reachable via VPC, provide the subnets selection here + * + * @default - none, required if setting vpc + */ + readonly vpcSubnets?: SubnetSelection, + + /** + * If your Kafka brokers are only reachable via VPC, provide the security group here + * + * @default - none, required if setting vpc + */ + readonly securityGroup?: ISecurityGroup + + /** + * The authentication method for your Kafka cluster + * + * @default AuthenticationMethod.SASL_SCRAM_512_AUTH + */ + readonly authenticationMethod?: AuthenticationMethod +} + +/** + * Use a MSK cluster as a streaming source for AWS Lambda + */ +export class ManagedKafkaEventSource extends StreamEventSource { + // This is to work around JSII inheritance problems + private innerProps: ManagedKafkaEventSourceProps; + + constructor(props: ManagedKafkaEventSourceProps) { + super(props); + this.innerProps = props; + } + + public bind(target: lambda.IFunction) { + target.addEventSourceMapping( + `KafkaEventSource:${this.innerProps.cluster.clusterArn}${this.innerProps.topic}`, + this.enrichMappingOptions({ + eventSourceArn: this.innerProps.cluster.clusterArn, + startingPosition: this.innerProps.startingPosition, + // From https://docs.aws.amazon.com/msk/latest/developerguide/msk-password.html#msk-password-limitations, "Amazon MSK only supports SCRAM-SHA-512 authentication." + sourceAccessConfigurations: [{ type: lambda.SourceAccessConfigurationType.SASL_SCRAM_512_AUTH, uri: this.innerProps.secret.secretArn }], + kafkaTopic: this.innerProps.topic, + }), + ); + + this.innerProps.secret.grantRead(target); + + target.addToRolePolicy(new iam.PolicyStatement( + { + actions: ['kafka:DescribeCluster', 'kafka:GetBootstrapBrokers', 'kafka:ListScramSecrets'], + resources: [this.innerProps.cluster.clusterArn], + }, + )); + + target.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaMSKExecutionRole')); + } +} + +/** + * Use a self hosted Kafka installation as a streaming source for AWS Lambda. + */ +export class SelfManagedKafkaEventSource extends StreamEventSource { + // This is to work around JSII inheritance problems + private innerProps: SelfManagedKafkaEventSourceProps; + + constructor(props: SelfManagedKafkaEventSourceProps) { + super(props); + if (props.vpc) { + if (!props.securityGroup) { + throw new Error('securityGroup must be set when providing vpc'); + } + if (!props.vpcSubnets) { + throw new Error('vpcSubnets must be set when providing vpc'); + } + } + this.innerProps = props; + } + + public bind(target: lambda.IFunction) { + if (!(target instanceof Construct)) { throw new Error('Function is not a construct. Unexpected error.'); } + target.addEventSourceMapping( + this.mappingId(target), + this.enrichMappingOptions({ + kafkaBootstrapServers: this.innerProps.bootstrapServers, + kafkaTopic: this.innerProps.topic, + startingPosition: this.innerProps.startingPosition, + sourceAccessConfigurations: this.sourceAccessConfigurations(), + }), + ); + this.innerProps.secret.grantRead(target); + } + + private mappingId(target: lambda.IFunction) { + let hash = crypto.createHash('md5'); + hash.update(JSON.stringify(Stack.of(target).resolve(this.innerProps.bootstrapServers))); + const idHash = hash.digest('hex'); + return `KafkaEventSource:${idHash}:${this.innerProps.topic}`; + } + + private sourceAccessConfigurations() { + let authType; + switch (this.innerProps.authenticationMethod) { + case AuthenticationMethod.SASL_SCRAM_256_AUTH: + authType = lambda.SourceAccessConfigurationType.SASL_SCRAM_256_AUTH; + break; + case AuthenticationMethod.SASL_SCRAM_512_AUTH: + default: + authType = lambda.SourceAccessConfigurationType.SASL_SCRAM_512_AUTH; + break; + } + let sourceAccessConfigurations = [{ type: authType, uri: this.innerProps.secret.secretArn }]; + if (this.innerProps.vpcSubnets !== undefined && this.innerProps.securityGroup !== undefined) { + sourceAccessConfigurations.push({ + type: lambda.SourceAccessConfigurationType.VPC_SECURITY_GROUP, + uri: this.innerProps.securityGroup.securityGroupId, + }, + ); + this.innerProps.vpc?.selectSubnets(this.innerProps.vpcSubnets).subnetIds.forEach((id) => { + sourceAccessConfigurations.push({ type: lambda.SourceAccessConfigurationType.VPC_SUBNET, uri: id }); + }); + } + return sourceAccessConfigurations; + } +} diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/stream.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/stream.ts index d18eaaf3f947c..96907b97835fc 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/stream.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/stream.ts @@ -3,7 +3,7 @@ import { Duration } from '@aws-cdk/core'; /** * The set of properties for event sources that follow the streaming model, - * such as, Dynamo and Kinesis. + * such as, Dynamo, Kinesis and Kafka. */ export interface StreamEventSourceProps { /** diff --git a/packages/@aws-cdk/aws-lambda-event-sources/package.json b/packages/@aws-cdk/aws-lambda-event-sources/package.json index fa1e019a44403..f3f925729c8ee 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/package.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/package.json @@ -73,12 +73,15 @@ "dependencies": { "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-msk": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-notifications": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", @@ -89,12 +92,15 @@ "peerDependencies": { "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-msk": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-notifications": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.dynamodb.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.dynamodb.expected.json index 6a16698ea1668..406a123559d74 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.dynamodb.expected.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.dynamodb.expected.json @@ -72,13 +72,13 @@ "Code": { "ZipFile": "exports.handler = async function handler(event) {\n console.log('event:', JSON.stringify(event, undefined, 2));\n return { event };\n}" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "FServiceRole3AC82EE1", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -89,16 +89,16 @@ "FDynamoDBEventSourcelambdaeventsourcedynamodbT7967476AE652DA48": { "Type": "AWS::Lambda::EventSourceMapping", "Properties": { + "FunctionName": { + "Ref": "FC4345940" + }, + "BatchSize": 5, "EventSourceArn": { "Fn::GetAtt": [ "TD925BC7E", "StreamArn" ] }, - "FunctionName": { - "Ref": "FC4345940" - }, - "BatchSize": 5, "StartingPosition": "TRIM_HORIZON" } }, diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json index 670d7d05ced59..54bfa6f32f361 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.kinesis.expected.json @@ -79,13 +79,13 @@ "Code": { "ZipFile": "exports.handler = async function handler(event) {\n console.log('event:', JSON.stringify(event, undefined, 2));\n return { event };\n}" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "FServiceRole3AC82EE1", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -96,16 +96,16 @@ "FKinesisEventSourcelambdaeventsourcekinesisQ645CE7DB2D6BCCF5": { "Type": "AWS::Lambda::EventSourceMapping", "Properties": { + "FunctionName": { + "Ref": "FC4345940" + }, + "BatchSize": 100, "EventSourceArn": { "Fn::GetAtt": [ "Q63C6E3AB", "Arn" ] }, - "FunctionName": { - "Ref": "FC4345940" - }, - "BatchSize": 100, "StartingPosition": "TRIM_HORIZON" } }, diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json index 53b599aa22c5a..96f5a3e6a63d7 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json @@ -37,13 +37,13 @@ "Code": { "ZipFile": "exports.handler = async function handler(event) {\n console.log('event:', JSON.stringify(event, undefined, 2));\n return { event };\n}" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "FServiceRole3AC82EE1", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -185,7 +185,7 @@ "Arn" ] }, - "Runtime": "nodejs10.x", + "Runtime": "nodejs12.x", "Timeout": 300 }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sns.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sns.expected.json index f756b4707d4c1..9a9c44e67d95f 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sns.expected.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.sns.expected.json @@ -37,13 +37,13 @@ "Code": { "ZipFile": "exports.handler = async function handler(event) {\n console.log('event:', JSON.stringify(event, undefined, 2));\n return { event };\n}" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "FServiceRole3AC82EE1", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.kafka.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/test.kafka.ts new file mode 100644 index 0000000000000..9cf1af9e50507 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/test.kafka.ts @@ -0,0 +1,334 @@ +import { arrayWith, expect, haveResource } from '@aws-cdk/assert'; +import { SecurityGroup, SubnetType, Vpc } from '@aws-cdk/aws-ec2'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as msk from '@aws-cdk/aws-msk'; +import { Secret } from '@aws-cdk/aws-secretsmanager'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as sources from '../lib'; +import { TestFunction } from './test-function'; + +export = { + 'msk': { + 'default'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const cluster = msk.Cluster.fromClusterArn(stack, 'Cluster', 'some-arn'); + const kafkaTopic = 'some-topic'; + const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + + // WHEN + fn.addEventSource(new sources.ManagedKafkaEventSource( + { + cluster: cluster, + topic: kafkaTopic, + secret: secret, + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + })); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: { + Ref: 'SecretA720EF05', + }, + }, + { + Action: [ + 'kafka:DescribeCluster', + 'kafka:GetBootstrapBrokers', + 'kafka:ListScramSecrets', + ], + Effect: 'Allow', + Resource: cluster.clusterArn, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'FnServiceRoleDefaultPolicyC6A839BF', + Roles: [ + { + Ref: 'FnServiceRoleB9001A96', + }, + ], + })); + + expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + EventSourceArn: cluster.clusterArn, + FunctionName: { + Ref: 'Fn9270CBC0', + }, + BatchSize: 100, + StartingPosition: 'TRIM_HORIZON', + Topics: [ + kafkaTopic, + ], + SourceAccessConfigurations: [ + { + Type: 'SASL_SCRAM_512_AUTH', + URI: { + Ref: 'SecretA720EF05', + }, + }, + ], + })); + + test.done(); + }, + }, + + 'self managed kafka': { + 'default'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const kafkaTopic = 'some-topic'; + const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + const bootstrapServers = ['kafka-broker:9092']; + + // WHEN + fn.addEventSource(new sources.SelfManagedKafkaEventSource( + { + bootstrapServers: bootstrapServers, + topic: kafkaTopic, + secret: secret, + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + })); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: { + Ref: 'SecretA720EF05', + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'FnServiceRoleDefaultPolicyC6A839BF', + Roles: [ + { + Ref: 'FnServiceRoleB9001A96', + }, + ], + })); + + expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + FunctionName: { + Ref: 'Fn9270CBC0', + }, + BatchSize: 100, + SelfManagedEventSource: { + Endpoints: { + KafkaBootstrapServers: bootstrapServers, + }, + }, + StartingPosition: 'TRIM_HORIZON', + Topics: [ + kafkaTopic, + ], + SourceAccessConfigurations: [ + { + Type: 'SASL_SCRAM_512_AUTH', + URI: { + Ref: 'SecretA720EF05', + }, + }, + ], + })); + + test.done(); + }, + + VPC: { + 'correctly rendered in the stack'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const kafkaTopic = 'some-topic'; + const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + const bootstrapServers = ['kafka-broker:9092']; + const sg = SecurityGroup.fromSecurityGroupId(stack, 'SecurityGroup', 'sg-0123456789'); + const vpc = new Vpc(stack, 'Vpc'); + + // WHEN + fn.addEventSource(new sources.SelfManagedKafkaEventSource( + { + bootstrapServers: bootstrapServers, + topic: kafkaTopic, + secret: secret, + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + vpc: vpc, + vpcSubnets: { subnetType: SubnetType.PRIVATE }, + securityGroup: sg, + })); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: { + Ref: 'SecretA720EF05', + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'FnServiceRoleDefaultPolicyC6A839BF', + Roles: [ + { + Ref: 'FnServiceRoleB9001A96', + }, + ], + })); + + expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + FunctionName: { + Ref: 'Fn9270CBC0', + }, + BatchSize: 100, + SelfManagedEventSource: { + Endpoints: { + KafkaBootstrapServers: bootstrapServers, + }, + }, + StartingPosition: 'TRIM_HORIZON', + Topics: [ + kafkaTopic, + ], + SourceAccessConfigurations: [ + { + Type: 'SASL_SCRAM_512_AUTH', + URI: { + Ref: 'SecretA720EF05', + }, + }, + { + Type: 'VPC_SECURITY_GROUP', + URI: 'sg-0123456789', + }, + { + Type: 'VPC_SUBNET', + URI: { + Ref: 'VpcPrivateSubnet1Subnet536B997A', + }, + }, + { + Type: 'VPC_SUBNET', + URI: { + Ref: 'VpcPrivateSubnet2Subnet3788AAA1', + }, + }, + ], + })); + + test.done(); + }, + 'setting vpc requires vpcSubnets to be set'(test: Test) { + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const kafkaTopic = 'some-topic'; + const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + const bootstrapServers = ['kafka-broker:9092']; + const vpc = new Vpc(stack, 'Vpc'); + + test.throws(() => { + fn.addEventSource(new sources.SelfManagedKafkaEventSource( + { + bootstrapServers: bootstrapServers, + topic: kafkaTopic, + secret: secret, + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + vpc: vpc, + securityGroup: SecurityGroup.fromSecurityGroupId(stack, 'SecurityGroup', 'sg-0123456789'), + + })); + }, /vpcSubnets must be set/); + + test.done(); + }, + + 'setting vpc requires securityGroup to be set'(test: Test) { + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const kafkaTopic = 'some-topic'; + const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + const bootstrapServers = ['kafka-broker:9092']; + const vpc = new Vpc(stack, 'Vpc'); + + test.throws(() => { + fn.addEventSource(new sources.SelfManagedKafkaEventSource( + { + bootstrapServers: bootstrapServers, + topic: kafkaTopic, + secret: secret, + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + vpc: vpc, + vpcSubnets: { subnetType: SubnetType.PRIVATE }, + })); + }, /securityGroup must be set/); + + test.done(); + }, + }, + + 'using SCRAM-SHA-256'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const kafkaTopic = 'some-topic'; + const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + const bootstrapServers = ['kafka-broker:9092']; + const sg = SecurityGroup.fromSecurityGroupId(stack, 'SecurityGroup', 'sg-0123456789'); + const vpc = new Vpc(stack, 'Vpc'); + + // WHEN + fn.addEventSource(new sources.SelfManagedKafkaEventSource( + { + bootstrapServers: bootstrapServers, + topic: kafkaTopic, + secret: secret, + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + vpc: vpc, + vpcSubnets: { subnetType: SubnetType.PRIVATE }, + securityGroup: sg, + authenticationMethod: sources.AuthenticationMethod.SASL_SCRAM_256_AUTH, + })); + + expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + SourceAccessConfigurations: arrayWith( + { + Type: 'SASL_SCRAM_256_AUTH', + URI: { + Ref: 'SecretA720EF05', + }, + }, + ), + })); + + test.done(); + }, + }, + +} diff --git a/packages/@aws-cdk/aws-lambda/lib/alias.ts b/packages/@aws-cdk/aws-lambda/lib/alias.ts index 3e22118644ac6..4287ffde73d6e 100644 --- a/packages/@aws-cdk/aws-lambda/lib/alias.ts +++ b/packages/@aws-cdk/aws-lambda/lib/alias.ts @@ -196,7 +196,7 @@ export class Alias extends QualifiedFunctionBase implements IAlias { } public metric(metricName: string, props: cloudwatch.MetricOptions = {}): cloudwatch.Metric { - // Metrics on Aliases need the "bare" function name, and the alias' ARN, this differes from the base behavior. + // Metrics on Aliases need the "bare" function name, and the alias' ARN, this differs from the base behavior. return super.metric(metricName, { dimensions: { FunctionName: this.lambda.functionName, diff --git a/packages/@aws-cdk/aws-lambda/lib/code.ts b/packages/@aws-cdk/aws-lambda/lib/code.ts index 0ac45f2e13d55..6557a6786065a 100644 --- a/packages/@aws-cdk/aws-lambda/lib/code.ts +++ b/packages/@aws-cdk/aws-lambda/lib/code.ts @@ -57,16 +57,26 @@ export abstract class Code { /** * Loads the function code from an asset created by a Docker build. * - * By defaut, the asset is expected to be located at `/asset` in the + * By default, the asset is expected to be located at `/asset` in the * image. * * @param path The path to the directory containing the Docker file * @param options Docker build options */ public static fromDockerBuild(path: string, options: DockerBuildAssetOptions = {}): AssetCode { + let imagePath = options.imagePath ?? '/asset/.'; + + // ensure imagePath ends with /. to copy the **content** at this path + if (imagePath.endsWith('/')) { + imagePath = `${imagePath}.`; + } else if (!imagePath.endsWith('/.')) { + imagePath = `${imagePath}/.`; + } + const assetPath = cdk.DockerImage .fromBuild(path, options) - .cp(options.imagePath ?? '/asset', options.outputPath); + .cp(imagePath, options.outputPath); + return new AssetCode(assetPath); } diff --git a/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts b/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts index d44ce1cbea1b4..239bf58671b7e 100644 --- a/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts +++ b/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts @@ -4,12 +4,78 @@ import { IEventSourceDlq } from './dlq'; import { IFunction } from './function-base'; import { CfnEventSourceMapping } from './lambda.generated'; +/** + * The type of authentication protocol or the VPC components for your event source's SourceAccessConfiguration + * @see https://docs.aws.amazon.com/lambda/latest/dg/API_SourceAccessConfiguration.html#SSS-Type-SourceAccessConfiguration-Type + */ +export class SourceAccessConfigurationType { + + /** + * (MQ) The Secrets Manager secret that stores your broker credentials. + */ + public static readonly BASIC_AUTH = new SourceAccessConfigurationType('BASIC_AUTH'); + + /** + * The subnets associated with your VPC. Lambda connects to these subnets to fetch data from your Self-Managed Apache Kafka cluster. + */ + public static readonly VPC_SUBNET = new SourceAccessConfigurationType('VPC_SUBNET'); + + /** + * The VPC security group used to manage access to your Self-Managed Apache Kafka brokers. + */ + public static readonly VPC_SECURITY_GROUP = new SourceAccessConfigurationType('VPC_SECURITY_GROUP'); + + /** + * The Secrets Manager ARN of your secret key used for SASL SCRAM-256 authentication of your Self-Managed Apache Kafka brokers. + */ + public static readonly SASL_SCRAM_256_AUTH = new SourceAccessConfigurationType('SASL_SCRAM_256_AUTH'); + + /** + * The Secrets Manager ARN of your secret key used for SASL SCRAM-512 authentication of your Self-Managed Apache Kafka brokers. + */ + public static readonly SASL_SCRAM_512_AUTH = new SourceAccessConfigurationType('SASL_SCRAM_512_AUTH'); + + /** A custom source access configuration property */ + public static of(name: string): SourceAccessConfigurationType { + return new SourceAccessConfigurationType(name); + } + + /** + * The key to use in `SourceAccessConfigurationProperty.Type` property in CloudFormation + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-eventsourcemapping-sourceaccessconfiguration.html#cfn-lambda-eventsourcemapping-sourceaccessconfiguration-type + */ + public readonly type: string; + + private constructor(type: string) { + this.type = type; + } +} + +/** + * Specific settings like the authentication protocol or the VPC components to secure access to your event source. + */ +export interface SourceAccessConfiguration { + /** + * The type of authentication protocol or the VPC components for your event source. For example: "SASL_SCRAM_512_AUTH". + */ + readonly type: SourceAccessConfigurationType, + /** + * The value for your chosen configuration in type. + * For example: "URI": "arn:aws:secretsmanager:us-east-1:01234567890:secret:MyBrokerSecretName". + * The exact string depends on the type. + * @see SourceAccessConfigurationType + */ + readonly uri: string +} + export interface EventSourceMappingOptions { /** * The Amazon Resource Name (ARN) of the event source. Any record added to * this stream can invoke the Lambda function. + * + * @default - not set if using a self managed Kafka cluster, throws an error otherwise */ - readonly eventSourceArn: string; + readonly eventSourceArn?: string; /** * The largest number of records that AWS Lambda will retrieve from your event @@ -101,6 +167,23 @@ export interface EventSourceMappingOptions { * @default - no topic */ readonly kafkaTopic?: string; + + /** + * A list of host and port pairs that are the addresses of the Kafka brokers in a self managed "bootstrap" Kafka cluster + * that a Kafka client connects to initially to bootstrap itself. + * They are in the format `abc.example.com:9096`. + * + * @default - none + */ + readonly kafkaBootstrapServers?: string[] + + /** + * Specific settings like the authentication protocol or the VPC components to secure access to your event source. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-eventsourcemapping-sourceaccessconfiguration.html + * + * @default - none + */ + readonly sourceAccessConfigurations?: SourceAccessConfiguration[] } /** @@ -154,6 +237,18 @@ export class EventSourceMapping extends cdk.Resource implements IEventSourceMapp constructor(scope: Construct, id: string, props: EventSourceMappingProps) { super(scope, id); + if (props.eventSourceArn == undefined && props.kafkaBootstrapServers == undefined) { + throw new Error('Either eventSourceArn or kafkaBootstrapServers must be set'); + } + + if (props.eventSourceArn !== undefined && props.kafkaBootstrapServers !== undefined) { + throw new Error('eventSourceArn and kafkaBootstrapServers are mutually exclusive'); + } + + if (props.kafkaBootstrapServers && (props.kafkaBootstrapServers?.length < 1)) { + throw new Error('kafkaBootStrapServers must not be empty if set'); + } + if (props.maxBatchingWindow && props.maxBatchingWindow.toSeconds() > 300) { throw new Error(`maxBatchingWindow cannot be over 300 seconds, got ${props.maxBatchingWindow.toSeconds()}`); } @@ -183,6 +278,11 @@ export class EventSourceMapping extends cdk.Resource implements IEventSourceMapp }; } + let selfManagedEventSource; + if (props.kafkaBootstrapServers) { + selfManagedEventSource = { endpoints: { kafkaBootstrapServers: props.kafkaBootstrapServers } }; + } + const cfnEventSourceMapping = new CfnEventSourceMapping(this, 'Resource', { batchSize: props.batchSize, bisectBatchOnFunctionError: props.bisectBatchOnError, @@ -196,6 +296,8 @@ export class EventSourceMapping extends cdk.Resource implements IEventSourceMapp maximumRetryAttempts: props.retryAttempts, parallelizationFactor: props.parallelizationFactor, topics: props.kafkaTopic !== undefined ? [props.kafkaTopic] : undefined, + sourceAccessConfigurations: props.sourceAccessConfigurations?.map((o) => {return { type: o.type.type, uri: o.uri };}), + selfManagedEventSource, }); this.eventSourceMappingId = cfnEventSourceMapping.ref; } diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index 8d487276a6176..1f9857ac35b1a 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -214,7 +214,7 @@ export interface FunctionOptions extends EventInvokeConfigOptions { /** * A list of layers to add to the function's execution environment. You can configure your Lambda function to pull in * additional code during initialization in the form of layers. Layers are packages of libraries or other dependencies - * that can be used by mulitple functions. + * that can be used by multiple functions. * * @default - No layers. */ @@ -563,7 +563,7 @@ export class Function extends FunctionBase { }); this.grantPrincipal = this.role; - // add additonal managed policies when necessary + // add additional managed policies when necessary if (props.filesystem) { const config = props.filesystem.config; if (config.policies) { diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts b/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts index b51d866ac815d..94f3e0b326b16 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda-version.ts @@ -221,7 +221,7 @@ export class Version extends QualifiedFunctionBase implements IVersion { } public metric(metricName: string, props: cloudwatch.MetricOptions = {}): cloudwatch.Metric { - // Metrics on Aliases need the "bare" function name, and the alias' ARN, this differes from the base behavior. + // Metrics on Aliases need the "bare" function name, and the alias' ARN, this differs from the base behavior. return super.metric(metricName, { dimensions: { FunctionName: this.lambda.functionName, diff --git a/packages/@aws-cdk/aws-lambda/test/code.test.ts b/packages/@aws-cdk/aws-lambda/test/code.test.ts index 91de07a17c5a6..c976f0a1dabf2 100644 --- a/packages/@aws-cdk/aws-lambda/test/code.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/code.test.ts @@ -331,6 +331,23 @@ describe('code', () => { }); describe('lambda.Code.fromDockerBuild', () => { + let fromBuildMock: jest.SpyInstance; + let cpMock: jest.Mock; + + beforeEach(() => { + cpMock = jest.fn().mockReturnValue(path.join(__dirname, 'docker-build-lambda')); + fromBuildMock = jest.spyOn(cdk.DockerImage, 'fromBuild').mockImplementation(() => ({ + cp: cpMock, + image: 'tag', + run: jest.fn(), + toJSON: jest.fn(), + })); + }); + + afterEach(() => { + fromBuildMock.mockRestore(); + }); + test('can use the result of a Docker build as an asset', () => { // given const stack = new cdk.Stack(); @@ -346,10 +363,47 @@ describe('code', () => { // then expect(stack).toHaveResource('AWS::Lambda::Function', { Metadata: { - [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.38cd320fa97b348accac88e48d9cede4923f7cab270ce794c95a665be83681a8', + [cxapi.ASSET_RESOURCE_METADATA_PATH_KEY]: 'asset.fbafdbb9ae8d1bae0def415b791a93c486d18ebc63270c748abecc3ac0ab9533', [cxapi.ASSET_RESOURCE_METADATA_PROPERTY_KEY]: 'Code', }, }, ResourcePart.CompleteDefinition); + + expect(fromBuildMock).toHaveBeenCalledWith(path.join(__dirname, 'docker-build-lambda'), {}); + expect(cpMock).toHaveBeenCalledWith('/asset/.', undefined); + }); + + test('fromDockerBuild appends /. to an image path not ending with a /', () => { + // given + const stack = new cdk.Stack(); + + // when + new lambda.Function(stack, 'Fn', { + code: lambda.Code.fromDockerBuild(path.join(__dirname, 'docker-build-lambda'), { + imagePath: '/my/image/path', + }), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_12_X, + }); + + // then + expect(cpMock).toHaveBeenCalledWith('/my/image/path/.', undefined); + }); + + test('fromDockerBuild appends . to an image path ending with a /', () => { + // given + const stack = new cdk.Stack(); + + // when + new lambda.Function(stack, 'Fn', { + code: lambda.Code.fromDockerBuild(path.join(__dirname, 'docker-build-lambda'), { + imagePath: '/my/image/path/', + }), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_12_X, + }); + + // then + expect(cpMock).toHaveBeenCalledWith('/my/image/path/.', undefined); }); }); }); diff --git a/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile index 4643fde141850..f22181359dc11 100644 --- a/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile +++ b/packages/@aws-cdk/aws-lambda/test/docker-build-lambda/Dockerfile @@ -1,3 +1,3 @@ FROM public.ecr.aws/amazonlinux/amazonlinux:latest -COPY index.js /asset +COPY index.js /asset/ diff --git a/packages/@aws-cdk/aws-lambda/test/event-source-mapping.test.ts b/packages/@aws-cdk/aws-lambda/test/event-source-mapping.test.ts index be42067f263f1..5d833a2d865c6 100644 --- a/packages/@aws-cdk/aws-lambda/test/event-source-mapping.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/event-source-mapping.test.ts @@ -170,4 +170,95 @@ describe('event source mapping', () => { }], }); }); + + test('throws if neither eventSourceArn nor kafkaBootstrapServers are set', () => { + const stack = new cdk.Stack(); + const fn = new Function(stack, 'fn', { + handler: 'index.handler', + code: Code.fromInline('exports.handler = ${handler.toString()}'), + runtime: Runtime.NODEJS_10_X, + }); + + expect(() => new EventSourceMapping(stack, 'test', { + target: fn, + })).toThrow(/Either eventSourceArn or kafkaBootstrapServers must be set/); + }); + + test('throws if both eventSourceArn and kafkaBootstrapServers are set', () => { + const stack = new cdk.Stack(); + const fn = new Function(stack, 'fn', { + handler: 'index.handler', + code: Code.fromInline('exports.handler = ${handler.toString()}'), + runtime: Runtime.NODEJS_10_X, + }); + + expect(() => new EventSourceMapping(stack, 'test', { + eventSourceArn: '', + kafkaBootstrapServers: [], + target: fn, + })).toThrow(/eventSourceArn and kafkaBootstrapServers are mutually exclusive/); + }); + + test('throws if both kafkaBootstrapServers is set but empty', () => { + const stack = new cdk.Stack(); + const fn = new Function(stack, 'fn', { + handler: 'index.handler', + code: Code.fromInline('exports.handler = ${handler.toString()}'), + runtime: Runtime.NODEJS_10_X, + }); + + expect(() => new EventSourceMapping(stack, 'test', { + kafkaBootstrapServers: [], + target: fn, + })).toThrow(/kafkaBootStrapServers must not be empty if set/); + }); + + test('eventSourceArn appears in stack', () => { + const stack = new cdk.Stack(); + const topicNameParam = new cdk.CfnParameter(stack, 'TopicNameParam', { + type: 'String', + }); + + const fn = new Function(stack, 'fn', { + handler: 'index.handler', + code: Code.fromInline('exports.handler = ${handler.toString()}'), + runtime: Runtime.NODEJS_10_X, + }); + + let eventSourceArn = 'some-arn'; + + new EventSourceMapping(stack, 'test', { + target: fn, + eventSourceArn: eventSourceArn, + kafkaTopic: topicNameParam.valueAsString, + }); + + expect(stack).toHaveResourceLike('AWS::Lambda::EventSourceMapping', { + EventSourceArn: eventSourceArn, + }); + }); + + test('kafkaBootstrapServers appears in stack', () => { + const stack = new cdk.Stack(); + const topicNameParam = new cdk.CfnParameter(stack, 'TopicNameParam', { + type: 'String', + }); + + const fn = new Function(stack, 'fn', { + handler: 'index.handler', + code: Code.fromInline('exports.handler = ${handler.toString()}'), + runtime: Runtime.NODEJS_10_X, + }); + + let kafkaBootstrapServers = ['kafka-broker.example.com:9092']; + new EventSourceMapping(stack, 'test', { + target: fn, + kafkaBootstrapServers: kafkaBootstrapServers, + kafkaTopic: topicNameParam.valueAsString, + }); + + expect(stack).toHaveResourceLike('AWS::Lambda::EventSourceMapping', { + SelfManagedEventSource: { Endpoints: { KafkaBootstrapServers: kafkaBootstrapServers } }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-msk/README.md b/packages/@aws-cdk/aws-msk/README.md index 1de05861fc74f..51d93453f1eef 100644 --- a/packages/@aws-cdk/aws-msk/README.md +++ b/packages/@aws-cdk/aws-msk/README.md @@ -9,6 +9,14 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- diff --git a/packages/@aws-cdk/aws-msk/lib/cluster.ts b/packages/@aws-cdk/aws-msk/lib/cluster.ts new file mode 100644 index 0000000000000..313bd3de5b107 --- /dev/null +++ b/packages/@aws-cdk/aws-msk/lib/cluster.ts @@ -0,0 +1,34 @@ +import { IResource, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +/** + * Represents an MSK cluster + */ +export interface ICluster extends IResource { + /** + * the ARN of the MSK cluster + */ + readonly clusterArn: string; +} + +/** + * An MSK cluster + */ +export class Cluster { + /** + * Creates a Cluster construct that represents an existing MSK cluster. + * @param scope + * @param id + * @param clusterArn + */ + public static fromClusterArn(scope: Construct, id: string, clusterArn: string): ICluster { + class Imported extends Resource implements ICluster { + public readonly clusterArn: string; + constructor() { + super(scope, id); + this.clusterArn = clusterArn; + } + } + return new Imported(); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-msk/lib/index.ts b/packages/@aws-cdk/aws-msk/lib/index.ts index 3cf150cbd7076..9cc4acdc7d61f 100644 --- a/packages/@aws-cdk/aws-msk/lib/index.ts +++ b/packages/@aws-cdk/aws-msk/lib/index.ts @@ -1,2 +1,3 @@ +export * from './cluster'; // AWS::MSK CloudFormation Resources: export * from './msk.generated'; diff --git a/packages/@aws-cdk/aws-msk/package.json b/packages/@aws-cdk/aws-msk/package.json index b922683011fe6..117d80e38cfed 100644 --- a/packages/@aws-cdk/aws-msk/package.json +++ b/packages/@aws-cdk/aws-msk/package.json @@ -91,7 +91,7 @@ "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-route53/README.md b/packages/@aws-cdk/aws-route53/README.md index aa5bf109b7473..9f36aa7f58765 100644 --- a/packages/@aws-cdk/aws-route53/README.md +++ b/packages/@aws-cdk/aws-route53/README.md @@ -191,7 +191,7 @@ This DNS name can also be guaranteed to match up with the backend certificate. Before consumers can use the private DNS name, you must verify that you have control of the domain/subdomain. -Assuming your account has ownership of the particlar domain/subdomain, +Assuming your account has ownership of the particular domain/subdomain, this construct sets up the private DNS configuration on the endpoint service, creates all the necessary Route53 entries, and verifies domain ownership. diff --git a/packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts b/packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts index 8705927249ed5..0576a9f89ddc9 100644 --- a/packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts +++ b/packages/@aws-cdk/aws-route53/lib/vpc-endpoint-service-domain-name.ts @@ -134,7 +134,7 @@ export class VpcEndpointServiceDomainName extends Construct { // Create the custom resource to look up the name/value pair generated by AWS // after the previous API call - const retriveNameValuePairAction = { + const retrieveNameValuePairAction = { service: 'EC2', action: 'describeVpcEndpointServiceConfigurations', parameters: { @@ -143,8 +143,8 @@ export class VpcEndpointServiceDomainName extends Construct { physicalResourceId: PhysicalResourceId.of(lookup), }; const getNames = new AwsCustomResource(this, 'GetNames', { - onCreate: retriveNameValuePairAction, - onUpdate: retriveNameValuePairAction, + onCreate: retrieveNameValuePairAction, + onUpdate: retrieveNameValuePairAction, // describeVpcEndpointServiceConfigurations can't take an ARN for granular permissions policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE, diff --git a/packages/@aws-cdk/aws-s3-assets/README.md b/packages/@aws-cdk/aws-s3-assets/README.md index 7a751410a2b22..8dae008aa5444 100644 --- a/packages/@aws-cdk/aws-s3-assets/README.md +++ b/packages/@aws-cdk/aws-s3-assets/README.md @@ -143,7 +143,7 @@ const asset = new assets.Asset(this, 'BundledAsset', { ``` Use `BundlingOutput.ARCHIVED` if the bundling output contains a single archive file and -you don't want it to be zippped. +you don't want it to be zipped. ## CloudFormation Resource Metadata diff --git a/packages/@aws-cdk/aws-s3-notifications/test/integ.notifications.expected.json b/packages/@aws-cdk/aws-s3-notifications/test/integ.notifications.expected.json index 4e12e4dd9badf..f8b1a350486f9 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/integ.notifications.expected.json +++ b/packages/@aws-cdk/aws-s3-notifications/test/integ.notifications.expected.json @@ -220,7 +220,7 @@ "Arn" ] }, - "Runtime": "nodejs10.x", + "Runtime": "nodejs12.x", "Timeout": 300 }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-s3-notifications/test/lambda/integ.bucket-notifications.expected.json b/packages/@aws-cdk/aws-s3-notifications/test/lambda/integ.bucket-notifications.expected.json index 8a1c134ff2651..8eb330f53a91e 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/lambda/integ.bucket-notifications.expected.json +++ b/packages/@aws-cdk/aws-s3-notifications/test/lambda/integ.bucket-notifications.expected.json @@ -106,13 +106,13 @@ "Code": { "ZipFile": "exports.handler = function handler(event, _context, callback) {\n console.log(JSON.stringify(event, undefined, 2));\n return callback(null, event);\n}" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "MyFunctionServiceRole3C357FF2", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -244,7 +244,7 @@ "Arn" ] }, - "Runtime": "nodejs10.x", + "Runtime": "nodejs12.x", "Timeout": 300 }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-s3-notifications/test/sns/integ.sns-bucket-notifications.expected.json b/packages/@aws-cdk/aws-s3-notifications/test/sns/integ.sns-bucket-notifications.expected.json index 0aabfdd9f8e19..2cd8f17706a09 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/sns/integ.sns-bucket-notifications.expected.json +++ b/packages/@aws-cdk/aws-s3-notifications/test/sns/integ.sns-bucket-notifications.expected.json @@ -203,7 +203,7 @@ "Arn" ] }, - "Runtime": "nodejs10.x", + "Runtime": "nodejs12.x", "Timeout": 300 }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-s3-notifications/test/sqs/integ.bucket-notifications.expected.json b/packages/@aws-cdk/aws-s3-notifications/test/sqs/integ.bucket-notifications.expected.json index 0da38cd2026c5..0e8a6a56cfb6e 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/sqs/integ.bucket-notifications.expected.json +++ b/packages/@aws-cdk/aws-s3-notifications/test/sqs/integ.bucket-notifications.expected.json @@ -192,7 +192,7 @@ "Arn" ] }, - "Runtime": "nodejs10.x", + "Runtime": "nodejs12.x", "Timeout": 300 }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 2cbf83682e595..e0dbf107c662b 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -183,7 +183,7 @@ export interface IBucket extends IResource { grantPutAcl(identity: iam.IGrantable, objectsKeyPattern?: string): iam.Grant; /** - * Grants s3:DeleteObject* permission to an IAM pricipal for objects + * Grants s3:DeleteObject* permission to an IAM principal for objects * in this bucket. * * @param identity The principal @@ -628,7 +628,7 @@ abstract class BucketBase extends Resource implements IBucket { } /** - * Grants s3:DeleteObject* permission to an IAM pricipal for objects + * Grants s3:DeleteObject* permission to an IAM principal for objects * in this bucket. * * @param identity The principal @@ -1620,7 +1620,7 @@ export class Bucket extends BucketBase { } /** - * Parse the lifecycle configuration out of the uucket props + * Parse the lifecycle configuration out of the bucket props * @param props Par */ private parseLifecycleConfiguration(): CfnBucket.LifecycleConfigurationProperty | undefined { diff --git a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts index 32aeab2267b7f..48ac9d2341abe 100644 --- a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts +++ b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts @@ -79,7 +79,7 @@ export class NotificationsResourceHandler extends Construct { Code: { ZipFile: `exports.handler = ${handler.toString()};` }, Handler: 'index.handler', Role: role.roleArn, - Runtime: 'nodejs10.x', + Runtime: 'nodejs12.x', Timeout: 300, }, }); diff --git a/packages/@aws-cdk/aws-s3outposts/.eslintrc.js b/packages/@aws-cdk/aws-s3outposts/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/aws-s3outposts/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-s3outposts/.gitignore b/packages/@aws-cdk/aws-s3outposts/.gitignore new file mode 100644 index 0000000000000..62ebc95d75ce6 --- /dev/null +++ b/packages/@aws-cdk/aws-s3outposts/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk +nyc.config.js +!.eslintrc.js +!jest.config.js +junit.xml diff --git a/packages/@aws-cdk/aws-s3outposts/.npmignore b/packages/@aws-cdk/aws-s3outposts/.npmignore new file mode 100644 index 0000000000000..e4486030fcb17 --- /dev/null +++ b/packages/@aws-cdk/aws-s3outposts/.npmignore @@ -0,0 +1,28 @@ +# Don't include original .ts files when doing `npm pack` +*.ts +!*.d.ts +coverage +.nyc_output +*.tgz + +dist +.LAST_PACKAGE +.LAST_BUILD +!*.js + +# Include .jsii +!.jsii + +*.snk + +*.tsbuildinfo + +tsconfig.json + +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ diff --git a/packages/@aws-cdk/aws-s3outposts/LICENSE b/packages/@aws-cdk/aws-s3outposts/LICENSE new file mode 100644 index 0000000000000..28e4bdcec77ec --- /dev/null +++ b/packages/@aws-cdk/aws-s3outposts/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-s3outposts/NOTICE b/packages/@aws-cdk/aws-s3outposts/NOTICE new file mode 100644 index 0000000000000..5fc3826926b5b --- /dev/null +++ b/packages/@aws-cdk/aws-s3outposts/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-s3outposts/README.md b/packages/@aws-cdk/aws-s3outposts/README.md new file mode 100644 index 0000000000000..08fc4b75a732a --- /dev/null +++ b/packages/@aws-cdk/aws-s3outposts/README.md @@ -0,0 +1,20 @@ +# AWS::S3Outposts Construct Library + + +--- + +![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge) + +> All classes with the `Cfn` prefix in this module ([CFN Resources]) are always stable and safe to use. +> +> [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib + +--- + + + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. + +```ts +import s3outposts = require('@aws-cdk/aws-s3outposts'); +``` diff --git a/packages/@aws-cdk/aws-s3outposts/jest.config.js b/packages/@aws-cdk/aws-s3outposts/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-s3outposts/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-s3outposts/lib/index.ts b/packages/@aws-cdk/aws-s3outposts/lib/index.ts new file mode 100644 index 0000000000000..06c96e7c920bc --- /dev/null +++ b/packages/@aws-cdk/aws-s3outposts/lib/index.ts @@ -0,0 +1,2 @@ +// AWS::S3Outposts CloudFormation Resources: +export * from './s3outposts.generated'; diff --git a/packages/@aws-cdk/aws-s3outposts/package.json b/packages/@aws-cdk/aws-s3outposts/package.json new file mode 100644 index 0000000000000..4c2236ec82a9c --- /dev/null +++ b/packages/@aws-cdk/aws-s3outposts/package.json @@ -0,0 +1,103 @@ +{ + "name": "@aws-cdk/aws-s3outposts", + "version": "0.0.0", + "description": "The CDK Construct Library for AWS::S3Outposts", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "projectReferences": true, + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.S3Outposts", + "packageId": "Amazon.CDK.AWS.S3Outposts", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.s3outposts", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "s3outposts" + } + }, + "python": { + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ], + "distName": "aws-cdk.aws-s3outposts", + "module": "aws_cdk.aws_s3outposts" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-s3outposts" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "compat": "cdk-compat", + "gen": "cfn2ts", + "rosetta:extract": "yarn --silent jsii-rosetta extract" + }, + "cdk-build": { + "cloudformation": "AWS::S3Outposts", + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": "true" + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "AWS::S3Outposts", + "aws-s3outposts" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "cdk-build-tools": "0.0.0", + "cfn2ts": "0.0.0", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0", + "constructs": "10.0.0-pre.5" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0", + "constructs": "10.0.0-pre.5" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "next" + }, + "private": true +} diff --git a/packages/@aws-cdk/aws-s3outposts/test/s3outposts.test.ts b/packages/@aws-cdk/aws-s3outposts/test/s3outposts.test.ts new file mode 100644 index 0000000000000..e394ef336bfb4 --- /dev/null +++ b/packages/@aws-cdk/aws-s3outposts/test/s3outposts.test.ts @@ -0,0 +1,6 @@ +import '@aws-cdk/assert/jest'; +import {} from '../lib'; + +test('No tests are specified for this package', () => { + expect(true).toBe(true); +}); diff --git a/packages/@aws-cdk/aws-sns/README.md b/packages/@aws-cdk/aws-sns/README.md index b5d9f52c3d9b9..ca80f72f9aad3 100644 --- a/packages/@aws-cdk/aws-sns/README.md +++ b/packages/@aws-cdk/aws-sns/README.md @@ -131,3 +131,43 @@ codeCommitRepository.onCommit(new targets.SnsTopic(myTopic)); This will result in adding a target to the event rule and will also modify the topic resource policy to allow CloudWatch events to publish to the topic. + +## Topic Policy + +A topic policy is automatically created when `addToResourcePolicy` is called, if +one doesn't already exist. Using `addToResourcePolicy` is the simplest way to +add policies, but a `TopicPolicy` can also be created manually. + +```ts +const topic = new sns.Topic(stack, 'Topic'); +const topicPolicy = new sns.TopicPolicy(stack, 'TopicPolicy', { + topics: [topic], +}); + +topicPolicy.document.addStatements(new iam.PolicyStatement({ + actions: ["sns:Subscribe"], + principals: [new iam.AnyPrincipal()], + resources: [topic.topicArn], +})); +``` + +A policy document can also be passed on `TopicPolicy` construction + +```ts +const topic = new sns.Topic(stack, 'Topic'); +const policyDocument = new iam.PolicyDocument({ + assignSids: true, + statements: [ + new iam.PolicyStatement({ + actions: ["sns:Subscribe"], + principals: [new iam.AnyPrincipal()], + resources: [topic.topicArn] + }), + ], +}); + +const topicPolicy = new sns.TopicPolicy(this, 'Policy', { + topics: [topic], + policyDocument, +}); +``` diff --git a/packages/@aws-cdk/aws-sns/lib/policy.ts b/packages/@aws-cdk/aws-sns/lib/policy.ts index 7d93d863a75f0..03a791bd57814 100644 --- a/packages/@aws-cdk/aws-sns/lib/policy.ts +++ b/packages/@aws-cdk/aws-sns/lib/policy.ts @@ -12,6 +12,12 @@ export interface TopicPolicyProps { * The set of topics this policy applies to. */ readonly topics: ITopic[]; + /** + * IAM policy document to apply to topic(s). + * @default empty policy document + */ + readonly policyDocument?: PolicyDocument; + } /** @@ -32,6 +38,8 @@ export class TopicPolicy extends Resource { constructor(scope: Construct, id: string, props: TopicPolicyProps) { super(scope, id); + this.document = props.policyDocument ?? this.document; + new CfnTopicPolicy(this, 'Resource', { policyDocument: this.document, topics: props.topics.map(t => t.topicArn), diff --git a/packages/@aws-cdk/aws-sns/test/test.sns.ts b/packages/@aws-cdk/aws-sns/test/test.sns.ts index cc4b50aed717c..5261900f9059b 100644 --- a/packages/@aws-cdk/aws-sns/test/test.sns.ts +++ b/packages/@aws-cdk/aws-sns/test/test.sns.ts @@ -276,6 +276,97 @@ export = { test.done(); }, + 'TopicPolicy passed document'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'MyTopic'); + const ps = new iam.PolicyStatement({ + actions: ['service:statement0'], + principals: [new iam.ArnPrincipal('arn')], + }); + + // WHEN + new sns.TopicPolicy(stack, 'topicpolicy', { topics: [topic], policyDocument: new iam.PolicyDocument({ assignSids: true, statements: [ps] }) }); + + // THEN + expect(stack).toMatch({ + 'Resources': { + 'MyTopic86869434': { + 'Type': 'AWS::SNS::Topic', + }, + 'topicpolicyF8CF12FD': { + 'Type': 'AWS::SNS::TopicPolicy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'service:statement0', + 'Effect': 'Allow', + 'Principal': { 'AWS': 'arn' }, + 'Sid': '0', + }, + ], + 'Version': '2012-10-17', + }, + 'Topics': [ + { + 'Ref': 'MyTopic86869434', + }, + ], + }, + }, + }, + }); + + test.done(); + }, + + 'Add statements to policy'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'MyTopic'); + + // WHEN + const topicPolicy = new sns.TopicPolicy(stack, 'TopicPolicy', { + topics: [topic], + }); + topicPolicy.document.addStatements(new iam.PolicyStatement({ + actions: ['service:statement0'], + principals: [new iam.ArnPrincipal('arn')], + })); + + // THEN + expect(stack).toMatch({ + 'Resources': { + 'MyTopic86869434': { + 'Type': 'AWS::SNS::Topic', + }, + 'TopicPolicyA24B096F': { + 'Type': 'AWS::SNS::TopicPolicy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'service:statement0', + 'Effect': 'Allow', + 'Principal': { 'AWS': 'arn' }, + 'Sid': '0', + }, + ], + 'Version': '2012-10-17', + }, + 'Topics': [ + { + 'Ref': 'MyTopic86869434', + }, + ], + }, + }, + }, + }); + test.done(); + }, + 'topic resource policy includes unique SIDs'(test: Test) { const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 69d97936aabcb..8d024db58e552 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -28,6 +28,9 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [ResultPath](#resultpath) - [Parameters](#task-parameters-from-the-state-json) - [Evaluate Expression](#evaluate-expression) +- [API Gateway](#api-gateway) + - [Call REST API Endpoint](#call-rest-api-endpoint) + - [Call HTTP API Endpoint](#call-http-api-endpoint) - [Athena](#athena) - [StartQueryExecution](#startQueryExecution) - [GetQueryExecution](#getQueryExecution) @@ -217,6 +220,47 @@ The `EvaluateExpression` supports a `runtime` prop to specify the Lambda runtime to use to evaluate the expression. Currently, only runtimes of the Node.js family are supported. +## API Gateway + +Step Functions supports [API Gateway](https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html) through the service integration pattern. + +HTTP APIs are designed for low-latency, cost-effective integrations with AWS services, including AWS Lambda, and HTTP endpoints. +HTTP APIs support OIDC and OAuth 2.0 authorization, and come with built-in support for CORS and automatic deployments. +Previous-generation REST APIs currently offer more features. More details can be found [here](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html). + +### Call REST API Endpoint + +The `CallApiGatewayRestApiEndpoint` calls the REST API endpoint. + +```ts +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from `@aws-cdk/aws-stepfunctions-tasks`; + +const restApi = new apigateway.RestApi(stack, 'MyRestApi'); + +const invokeTask = new tasks.CallApiGatewayRestApiEndpoint(stack, 'Call REST API', { + api: restApi, + stageName: 'prod', + method: HttpMethod.GET, +}); +``` + +### Call HTTP API Endpoint + +The `CallApiGatewayHttpApiEndpoint` calls the HTTP API endpoint. + +```ts +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from `@aws-cdk/aws-stepfunctions-tasks`; + +const httpApi = new apigatewayv2.HttpApi(stack, 'MyHttpApi'); + +const invokeTask = new tasks.CallApiGatewayHttpApiEndpoint(stack, 'Call HTTP API', { + api: httpApi, + method: HttpMethod.GET, +}); +``` + ## Athena Step Functions supports [Athena](https://docs.aws.amazon.com/step-functions/latest/dg/connect-athena.html) through the service integration pattern. diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base-types.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base-types.ts new file mode 100644 index 0000000000000..64c649063e57c --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base-types.ts @@ -0,0 +1,79 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; + +/** Http Methods that API Gateway supports */ +export enum HttpMethod { + /** Retreive data from a server at the specified resource */ + GET = 'GET', + + /** Send data to the API endpoint to create or udpate a resource */ + POST = 'POST', + + /** Send data to the API endpoint to update or create a resource */ + PUT = 'PUT', + + /** Delete the resource at the specified endpoint */ + DELETE = 'DELETE', + + /** Apply partial modifications to the resource */ + PATCH = 'PATCH', + + /** Retreive data from a server at the specified resource without the response body */ + HEAD = 'HEAD', + + /** Return data describing what other methods and operations the server supports */ + OPTIONS = 'OPTIONS' +} + +/** + * The authentication method used to call the endpoint + */ +export enum AuthType { + /** Call the API direclty with no authorization method */ + NO_AUTH = 'NO_AUTH', + + /** Use the IAM role associated with the current state machine for authorization */ + IAM_ROLE = 'IAM_ROLE', + + /** Use the resource policy of the API for authorization */ + RESOURCE_POLICY = 'RESOURCE_POLICY', +} + +/** + * Base CallApiGatewayEdnpoint Task Props + */ +export interface CallApiGatewayEndpointBaseProps extends sfn.TaskStateBaseProps { + /** + * Http method for the API + */ + readonly method: HttpMethod; + + /** + * HTTP request information that does not relate to contents of the request + * @default - No headers + */ + readonly headers?: sfn.TaskInput; + + /** + * Path parameters appended after API endpoint + * @default - No path + */ + readonly apiPath?: string; + + /** + * Query strings attatched to end of request + * @default - No query parameters + */ + readonly queryParameters?: sfn.TaskInput; + + /** + * HTTP Request body + * @default - No request body + */ + readonly requestBody?: sfn.TaskInput; + + /** + * Authentication methods + * @default AuthType.NO_AUTH + */ + readonly authType?: AuthType; +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base.ts new file mode 100644 index 0000000000000..edce3aa0f627c --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base.ts @@ -0,0 +1,69 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct } from 'constructs'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; +import { AuthType, CallApiGatewayEndpointBaseProps } from './base-types'; + +/** + * Base CallApiGatewayEndpoint Task + * @internal + */ +export abstract class CallApiGatewayEndpointBase extends sfn.TaskStateBase { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + ]; + + private readonly baseProps: CallApiGatewayEndpointBaseProps; + private readonly integrationPattern: sfn.IntegrationPattern; + + protected abstract readonly apiEndpoint: string; + protected abstract readonly arnForExecuteApi: string; + protected abstract readonly stageName?: string; + + constructor(scope: Construct, id: string, props: CallApiGatewayEndpointBaseProps) { + super(scope, id, props); + + this.baseProps = props; + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; + validatePatternSupported(this.integrationPattern, CallApiGatewayEndpointBase.SUPPORTED_INTEGRATION_PATTERNS); + + if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN) { + if (!sfn.FieldUtils.containsTaskToken(this.baseProps.headers)) { + throw new Error('Task Token is required in `headers` for WAIT_FOR_TASK_TOKEN pattern. Use JsonPath.taskToken to set the token.'); + } + } + } + + /** + * @internal + */ + protected _renderTask() { + return { + Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + ApiEndpoint: this.apiEndpoint, + Method: this.baseProps.method, + Headers: this.baseProps.headers?.value, + Stage: this.stageName, + Path: this.baseProps.apiPath, + QueryParameters: this.baseProps.queryParameters?.value, + RequestBody: this.baseProps.requestBody?.value, + AuthType: this.baseProps.authType ? this.baseProps.authType : 'NO_AUTH', + }), + }; + } + + protected createPolicyStatements(): iam.PolicyStatement[] { + if (this.baseProps.authType === AuthType.NO_AUTH) { + return []; + } + + return [ + new iam.PolicyStatement({ + resources: [this.arnForExecuteApi], + actions: ['execute-api:Invoke'], + }), + ]; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-http-api.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-http-api.ts new file mode 100644 index 0000000000000..e06e46c2580b0 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-http-api.ts @@ -0,0 +1,62 @@ +import * as apigatewayv2 from '@aws-cdk/aws-apigatewayv2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CallApiGatewayEndpointBase } from './base'; +import { CallApiGatewayEndpointBaseProps } from './base-types'; + +/** + * Properties for calling an HTTP API Endpoint + */ +export interface CallApiGatewayHttpApiEndpointProps extends CallApiGatewayEndpointBaseProps { + /** + * API to call + */ + readonly api: apigatewayv2.IHttpApi; + + /** + * Name of the stage where the API is deployed to in API Gateway + * @default '$default' + */ + readonly stageName?: string; +} + +/** + * Call HTTP API endpoint as a Task + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html + */ +export class CallApiGatewayHttpApiEndpoint extends CallApiGatewayEndpointBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig | undefined; + protected readonly taskPolicies?: iam.PolicyStatement[] | undefined; + + protected readonly apiEndpoint: string; + protected readonly arnForExecuteApi: string; + protected readonly stageName?: string; + + constructor(scope: Construct, id: string, private readonly props: CallApiGatewayHttpApiEndpointProps) { + super(scope, id, props); + + this.apiEndpoint = this.getApiEndpoint(); + this.arnForExecuteApi = this.getArnForExecuteApi(); + + this.taskPolicies = this.createPolicyStatements(); + } + + private getApiEndpoint(): string { + const apiStack = cdk.Stack.of(this.props.api); + return `${this.props.api.apiId}.execute-api.${apiStack.region}.${apiStack.urlSuffix}`; + } + + private getArnForExecuteApi(): string { + const { api, stageName, method, apiPath } = this.props; + + return cdk.Stack.of(api).formatArn({ + service: 'execute-api', + resource: api.apiId, + sep: '/', + resourceName: `${stageName}/${method}${apiPath}`, + }); + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-rest-api.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-rest-api.ts new file mode 100644 index 0000000000000..0352777e9c06a --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-rest-api.ts @@ -0,0 +1,51 @@ +import * as apigateway from '@aws-cdk/aws-apigateway'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CallApiGatewayEndpointBase } from './base'; +import { CallApiGatewayEndpointBaseProps } from './base-types'; + +/** + * Properties for calling an REST API Endpoint + */ +export interface CallApiGatewayRestApiEndpointProps extends CallApiGatewayEndpointBaseProps { + /** + * API to call + */ + readonly api: apigateway.IRestApi; + + /** + * Name of the stage where the API is deployed to in API Gateway + */ + readonly stageName: string; +} + +/** + * Call REST API endpoint as a Task + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html + */ +export class CallApiGatewayRestApiEndpoint extends CallApiGatewayEndpointBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig | undefined; + protected readonly taskPolicies?: iam.PolicyStatement[] | undefined; + + protected readonly apiEndpoint: string; + protected readonly arnForExecuteApi: string; + protected readonly stageName?: string; + + constructor(scope: Construct, id: string, private readonly props: CallApiGatewayRestApiEndpointProps) { + super(scope, id, props); + + this.apiEndpoint = this.getApiEndpoint(); + this.arnForExecuteApi = props.api.arnForExecuteApi(props.method, props.apiPath, props.stageName); + this.stageName = props.stageName; + + this.taskPolicies = this.createPolicyStatements(); + } + + private getApiEndpoint(): string { + const apiStack = cdk.Stack.of(this.props.api); + return `${this.props.api.restApiId}.execute-api.${apiStack.region}.${apiStack.urlSuffix}`; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/index.ts new file mode 100644 index 0000000000000..3d82ca2e7d548 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/index.ts @@ -0,0 +1,3 @@ +export * from './base-types'; +export * from './call-rest-api'; +export * from './call-http-api'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts index 64c25d5e3dd3a..cf36dc806ebfb 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts @@ -99,19 +99,28 @@ export class EvaluateExpression extends sfn.TaskStateBase { function createEvalFn(runtime: lambda.Runtime, scope: Construct) { const lambdaPurpose = 'Eval'; + const nodeJsGuids = { + [lambda.Runtime.NODEJS_14_X.name]: 'da2d1181-604e-4a45-8694-1a6abd7fe42d', + [lambda.Runtime.NODEJS_12_X.name]: '2b81e383-aad2-44db-8aaf-b4809ae0e3b4', + [lambda.Runtime.NODEJS_10_X.name]: 'a0d2ce44-871b-4e74-87a1-f5e63d7c3bdc', + }; + switch (runtime) { case lambda.Runtime.NODEJS_14_X: case lambda.Runtime.NODEJS_12_X: case lambda.Runtime.NODEJS_10_X: - return new lambda.SingletonFunction(scope, 'EvalFunction', { - runtime, - handler: 'index.handler', - uuid: 'a0d2ce44-871b-4e74-87a1-f5e63d7c3bdc', - lambdaPurpose, - code: lambda.Code.fromAsset(path.join(__dirname, 'eval-nodejs-handler')), - }); - // TODO: implement other runtimes - default: - throw new Error(`The runtime ${runtime.name} is currently not supported.`); + const uuid = nodeJsGuids[runtime.name]; + if (uuid) { + return new lambda.SingletonFunction(scope, 'EvalFunction', { + runtime, + uuid, + handler: 'index.handler', + lambdaPurpose, + code: lambda.Code.fromAsset(path.join(__dirname, 'eval-nodejs-handler')), + }); + } + break; } + + throw new Error(`The runtime ${runtime.name} is currently not supported.`); } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 32e684f6d1adf..7b566bbbe4dad 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -45,3 +45,4 @@ export * from './athena/get-query-execution'; export * from './athena/get-query-results'; export * from './databrew/start-job-run'; export * from './eks/call'; +export * from './apigateway'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index a64d0dc20c352..8aa8b66db9561 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -73,6 +73,9 @@ "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-apigateway": "0.0.0", + "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", @@ -96,6 +99,9 @@ }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-apigateway": "0.0.0", + "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-http-api.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-http-api.test.ts new file mode 100644 index 0000000000000..0e7a2cf616b9a --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-http-api.test.ts @@ -0,0 +1,145 @@ +import * as apigatewayv2 from '@aws-cdk/aws-apigatewayv2'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { HttpMethod, CallApiGatewayHttpApiEndpoint } from '../../lib'; + +describe('CallApiGatewayHttpApiEndpoint', () => { + test('default', () => { + // GIVEN + const stack = new cdk.Stack(); + const httpApi = new apigatewayv2.HttpApi(stack, 'HttpApi'); + + // WHEN + const task = new CallApiGatewayHttpApiEndpoint(stack, 'Call', { + api: httpApi, + method: HttpMethod.GET, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + End: true, + Parameters: { + ApiEndpoint: { + 'Fn::Join': [ + '', + [ + { + Ref: 'HttpApiF5A9A8A7', + }, + '.execute-api.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, + AuthType: 'NO_AUTH', + Method: 'GET', + }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke', + ], + ], + }, + }); + }); + + test('wait for task token', () => { + // GIVEN + const stack = new cdk.Stack(); + const httpApi = new apigatewayv2.HttpApi(stack, 'HttpApi'); + + // WHEN + const task = new CallApiGatewayHttpApiEndpoint(stack, 'Call', { + api: httpApi, + method: HttpMethod.GET, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + headers: sfn.TaskInput.fromObject({ TaskToken: sfn.JsonPath.taskToken }), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + End: true, + Parameters: { + ApiEndpoint: { + 'Fn::Join': [ + '', + [ + { + Ref: 'HttpApiF5A9A8A7', + }, + '.execute-api.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, + AuthType: 'NO_AUTH', + Headers: { + 'TaskToken.$': '$$.Task.Token', + }, + Method: 'GET', + }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke.waitForTaskToken', + ], + ], + }, + }); + }); + + test('wait for task token - missing token', () => { + // GIVEN + const stack = new cdk.Stack(); + const httpApi = new apigatewayv2.HttpApi(stack, 'HttpApi'); + + // THEN + expect(() => { + new CallApiGatewayHttpApiEndpoint(stack, 'Call', { + api: httpApi, + method: HttpMethod.GET, + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + }); + }).toThrow(/Task Token is required in `headers` for WAIT_FOR_TASK_TOKEN pattern. Use JsonPath.taskToken to set the token./); + }); + + test('unsupported integration pattern - RUN_JOB', () => { + // GIVEN + const stack = new cdk.Stack(); + const httpApi = new apigatewayv2.HttpApi(stack, 'HttpApi'); + + // THEN + expect(() => { + new CallApiGatewayHttpApiEndpoint(stack, 'Call', { + api: httpApi, + method: HttpMethod.GET, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }); + }).toThrow(/Unsupported service integration pattern./); + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-rest-api.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-rest-api.test.ts new file mode 100644 index 0000000000000..37a083fb2cc95 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-rest-api.test.ts @@ -0,0 +1,151 @@ +import * as apigateway from '@aws-cdk/aws-apigateway'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { HttpMethod, CallApiGatewayRestApiEndpoint } from '../../lib'; + +describe('CallApiGatewayRestApiEndpoint', () => { + test('default', () => { + // GIVEN + const stack = new cdk.Stack(); + const restApi = new apigateway.RestApi(stack, 'RestApi'); + + // WHEN + const task = new CallApiGatewayRestApiEndpoint(stack, 'Call', { + api: restApi, + method: HttpMethod.GET, + stageName: 'dev', + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + End: true, + Parameters: { + ApiEndpoint: { + 'Fn::Join': [ + '', + [ + { + Ref: 'RestApi0C43BF4B', + }, + '.execute-api.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, + AuthType: 'NO_AUTH', + Method: 'GET', + Stage: 'dev', + }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke', + ], + ], + }, + }); + }); + + test('wait for task token', () => { + // GIVEN + const stack = new cdk.Stack(); + const restApi = new apigateway.RestApi(stack, 'RestApi'); + + // WHEN + const task = new CallApiGatewayRestApiEndpoint(stack, 'Call', { + api: restApi, + method: HttpMethod.GET, + stageName: 'dev', + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + headers: sfn.TaskInput.fromObject({ TaskToken: sfn.JsonPath.taskToken }), + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + End: true, + Parameters: { + ApiEndpoint: { + 'Fn::Join': [ + '', + [ + { + Ref: 'RestApi0C43BF4B', + }, + '.execute-api.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, + AuthType: 'NO_AUTH', + Headers: { + 'TaskToken.$': '$$.Task.Token', + }, + Method: 'GET', + Stage: 'dev', + }, + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke.waitForTaskToken', + ], + ], + }, + }); + }); + + test('wait for task token - missing token', () => { + // GIVEN + const stack = new cdk.Stack(); + const restApi = new apigateway.RestApi(stack, 'RestApi'); + + // THEN + expect(() => { + new CallApiGatewayRestApiEndpoint(stack, 'Call', { + api: restApi, + method: HttpMethod.GET, + stageName: 'dev', + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + }); + }).toThrow(/Task Token is required in `headers` for WAIT_FOR_TASK_TOKEN pattern. Use JsonPath.taskToken to set the token./); + }); + + test('unsupported integration pattern - RUN_JOB', () => { + // GIVEN + const stack = new cdk.Stack(); + const restApi = new apigateway.RestApi(stack, 'RestApi'); + + // THEN + expect(() => { + new CallApiGatewayRestApiEndpoint(stack, 'Call', { + api: restApi, + method: HttpMethod.GET, + stageName: 'dev', + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }); + }).toThrow(/Unsupported service integration pattern./); + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.expected.json new file mode 100644 index 0000000000000..6afe44cfecda5 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.expected.json @@ -0,0 +1,263 @@ +{ + "Resources": { + "MyHttpApi8AEAAC21": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "MyHttpApi", + "ProtocolType": "HTTP" + } + }, + "MyHttpApiDefaultStageDCB9BC49": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "StageName": "$default", + "AutoDeploy": true + } + }, + "MyHttpApiANYCallHttpApiIntegMyHttpApiANY7E6F12A3Permission59116CA6": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "HelloHandler2E4FBA4D", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyHttpApi8AEAAC21" + }, + "/*/*/" + ] + ] + } + } + }, + "MyHttpApiANYHttpIntegration71abbf75d6f8e5ea93ec2120c0d78b754BBCECF5": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "HelloHandler2E4FBA4D", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + } + }, + "MyHttpApiANYC3543576": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "RouteKey": "ANY /", + "AuthorizationScopes": [], + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "MyHttpApiANYHttpIntegration71abbf75d6f8e5ea93ec2120c0d78b754BBCECF5" + } + ] + ] + } + } + }, + "HelloHandlerServiceRole11EF7C63": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "HelloHandler2E4FBA4D": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { return { statusCode: 200, body: \"hello, world!\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "HelloHandlerServiceRole11EF7C63", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "HelloHandlerServiceRole11EF7C63" + ] + }, + "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": "execute-api:Invoke", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyHttpApi8AEAAC21" + }, + "/undefined/GETundefined" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Call APIGW\",\"States\":{\"Call APIGW\":{\"End\":true,\"Type\":\"Task\",\"OutputPath\":\"$.ResponseBody\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::apigateway:invoke\",\"Parameters\":{\"ApiEndpoint\":\"", + { + "Ref": "MyHttpApi8AEAAC21" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "\",\"Method\":\"GET\",\"AuthType\":\"IAM_ROLE\"}}},\"TimeoutSeconds\":30}" + ] + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.ts new file mode 100644 index 0000000000000..4eb1f3b896e92 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.ts @@ -0,0 +1,48 @@ +import * as apigatewayv2 from '@aws-cdk/aws-apigatewayv2'; +import * as integrations from '@aws-cdk/aws-apigatewayv2-integrations'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { AuthType, HttpMethod, CallApiGatewayHttpApiEndpoint } from '../../lib'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --state-machine-arn : should return execution arn + * + * * aws stepfunctions describe-execution --execution-arn --query 'status': should return status as SUCCEEDED + * * aws stepfunctions describe-execution --execution-arn --query 'output': should return the string \"hello, world!\" + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'CallHttpApiInteg'); +const httpApi = new apigatewayv2.HttpApi(stack, 'MyHttpApi'); + +const handler = new lambda.Function(stack, 'HelloHandler', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { return { statusCode: 200, body: "hello, world!" }; };'), +}); +httpApi.addRoutes({ + path: '/', + integration: new integrations.LambdaProxyIntegration({ + handler, + }), +}); + +const callEndpointJob = new CallApiGatewayHttpApiEndpoint(stack, 'Call APIGW', { + api: httpApi, + method: HttpMethod.GET, + authType: AuthType.IAM_ROLE, + outputPath: sfn.JsonPath.stringAt('$.ResponseBody'), +}); + +const chain = sfn.Chain.start(callEndpointJob); + +const sm = new sfn.StateMachine(stack, 'StateMachine', { + definition: chain, + timeout: cdk.Duration.seconds(30), +}); + +new cdk.CfnOutput(stack, 'stateMachineArn', { + value: sm.stateMachineArn, +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.expected.json new file mode 100644 index 0000000000000..5970499935354 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.expected.json @@ -0,0 +1,394 @@ +{ + "Resources": { + "MyRestApi2D1F47A9": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "MyRestApi" + } + }, + "MyRestApiCloudWatchRoleD4042E8E": { + "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" + ] + ] + } + ] + } + }, + "MyRestApiAccount2FB6DB7A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "MyRestApiCloudWatchRoleD4042E8E", + "Arn" + ] + } + }, + "DependsOn": [ + "MyRestApi2D1F47A9" + ] + }, + "MyRestApiDeploymentB555B582d61dc696e12272a0706c826196fa8d62": { + "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": "MyRestApiDeploymentB555B582d61dc696e12272a0706c826196fa8d62" + }, + "StageName": "prod" + } + }, + "MyRestApiANYApiPermissionCallRestApiIntegMyRestApiB570839CANY0C27C1E3": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Hello4A628BD4", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyRestApi2D1F47A9" + }, + "/", + { + "Ref": "MyRestApiDeploymentStageprodC33B8E5F" + }, + "/*/" + ] + ] + } + } + }, + "MyRestApiANYApiPermissionTestCallRestApiIntegMyRestApiB570839CANY379723EF": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Hello4A628BD4", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyRestApi2D1F47A9" + }, + "/test-invoke-stage/*/" + ] + ] + } + } + }, + "MyRestApiANY05143F93": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "MyRestApi2D1F47A9", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "Hello4A628BD4", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "HelloServiceRole1E55EA16": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Hello4A628BD4": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function(event, context) { return { statusCode: 200, body: \"hello, world!\" }; };" + }, + "Role": { + "Fn::GetAtt": [ + "HelloServiceRole1E55EA16", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "HelloServiceRole1E55EA16" + ] + }, + "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": "execute-api:Invoke", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyRestApi2D1F47A9" + }, + "/prod/GET/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Call APIGW\",\"States\":{\"Call APIGW\":{\"End\":true,\"Type\":\"Task\",\"OutputPath\":\"$.ResponseBody\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::apigateway:invoke\",\"Parameters\":{\"ApiEndpoint\":\"", + { + "Ref": "MyRestApi2D1F47A9" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "\",\"Method\":\"GET\",\"Stage\":\"prod\",\"AuthType\":\"IAM_ROLE\"}}},\"TimeoutSeconds\":30}" + ] + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "MyRestApiEndpoint4C55E4CB": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "MyRestApi2D1F47A9" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "MyRestApiDeploymentStageprodC33B8E5F" + }, + "/" + ] + ] + } + }, + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.ts new file mode 100644 index 0000000000000..7cfe3c85ab12b --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.ts @@ -0,0 +1,43 @@ +import * as apigateway from '@aws-cdk/aws-apigateway'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { AuthType, HttpMethod, CallApiGatewayRestApiEndpoint } from '../../lib'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --state-machine-arn : should return execution arn + * + * * aws stepfunctions describe-execution --execution-arn --query 'status': should return status as SUCCEEDED + * * aws stepfunctions describe-execution --execution-arn --query 'output': should return the string \"hello, world!\" + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'CallRestApiInteg'); +const restApi = new apigateway.RestApi(stack, 'MyRestApi'); + +const hello = new apigateway.LambdaIntegration(new lambda.Function(stack, 'Hello', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: new lambda.InlineCode('exports.handler = async function(event, context) { return { statusCode: 200, body: "hello, world!" }; };'), +})); +restApi.root.addMethod('ANY', hello); + +const callEndpointJob = new CallApiGatewayRestApiEndpoint(stack, 'Call APIGW', { + api: restApi, + stageName: 'prod', + method: HttpMethod.GET, + authType: AuthType.IAM_ROLE, + outputPath: sfn.JsonPath.stringAt('$.ResponseBody'), +}); + +const chain = sfn.Chain.start(callEndpointJob); + +const sm = new sfn.StateMachine(stack, 'StateMachine', { + definition: chain, + timeout: cdk.Duration.seconds(30), +}); + +new cdk.CfnOutput(stack, 'stateMachineArn', { + value: sm.stateMachineArn, +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.run-batch-job.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.run-batch-job.expected.json index d7c5476ab3488..252eeef05dea4 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.run-batch-job.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.run-batch-job.expected.json @@ -988,6 +988,12 @@ "StateMachine2E01A3A5": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", @@ -1007,12 +1013,6 @@ ":states:::batch:submitJob.sync\"}}}" ] ] - }, - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRoleB840431D", - "Arn" - ] } }, "DependsOn": [ @@ -1033,4 +1033,4 @@ } } } -} +} \ No newline at end of file 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 index 165ab49ae11a7..6b45fb3889e9b 100644 --- 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 @@ -988,6 +988,12 @@ "StateMachine2E01A3A5": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", @@ -1007,12 +1013,6 @@ "\",\"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": [ @@ -1033,4 +1033,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/integ.start-build.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/integ.start-build.expected.json index ad853ea6241c3..432368a1de6d6 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/integ.start-build.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/integ.start-build.expected.json @@ -154,8 +154,8 @@ "BuildSpec": "{\n \"version\": \"0.2\",\n \"phases\": {\n \"build\": {\n \"commands\": [\n \"echo \\\"Hello, CodeBuild!\\\"\"\n ]\n }\n }\n}", "Type": "NO_SOURCE" }, - "Name": "MyTestProject", - "EncryptionKey": "alias/aws/s3" + "EncryptionKey": "alias/aws/s3", + "Name": "MyTestProject" } }, "StateMachineRoleB840431D": { @@ -261,4 +261,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.expected.json index 26ef883551481..2b574e0b9baa4 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.expected.json @@ -80,8 +80,6 @@ "ecs:Poll", "ecs:StartTelemetrySession" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -91,7 +89,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -268,8 +268,6 @@ "ecs:DescribeContainerInstances", "ecs:DescribeTasks" ], - "Effect": "Allow", - "Resource": "*", "Condition": { "ArnEquals": { "ecs:cluster": { @@ -279,7 +277,9 @@ ] } } - } + }, + "Effect": "Allow", + "Resource": "*" }, { "Action": [ @@ -330,14 +330,12 @@ "Code": { "ZipFile": "import boto3, json, os, time\n\necs = boto3.client('ecs')\nautoscaling = boto3.client('autoscaling')\n\n\ndef lambda_handler(event, context):\n print(json.dumps(event))\n cluster = os.environ['CLUSTER']\n snsTopicArn = event['Records'][0]['Sns']['TopicArn']\n lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])\n instance_id = lifecycle_event.get('EC2InstanceId')\n if not instance_id:\n print('Got event without EC2InstanceId: %s', json.dumps(event))\n return\n\n instance_arn = container_instance_arn(cluster, instance_id)\n print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))\n\n if not instance_arn:\n return\n\n while has_tasks(cluster, instance_arn):\n time.sleep(10)\n\n try:\n print('Terminating instance %s' % instance_id)\n autoscaling.complete_lifecycle_action(\n LifecycleActionResult='CONTINUE',\n **pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))\n except Exception as e:\n # Lifecycle action may have already completed.\n print(str(e))\n\n\ndef container_instance_arn(cluster, instance_id):\n \"\"\"Turn an instance ID into a container instance ARN.\"\"\"\n arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']\n if not arns:\n return None\n return arns[0]\n\n\ndef has_tasks(cluster, instance_arn):\n \"\"\"Return True if the instance is running tasks for the given cluster.\"\"\"\n instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']\n if not instances:\n return False\n instance = instances[0]\n\n if instance['status'] == 'ACTIVE':\n # Start draining, then try again later\n set_container_instance_to_draining(cluster, instance_arn)\n return True\n\n tasks = instance['runningTasksCount'] + instance['pendingTasksCount']\n print('Instance %s has %s tasks' % (instance_arn, tasks))\n\n return tasks > 0\n\n\ndef set_container_instance_to_draining(cluster, instance_arn):\n ecs.update_container_instances_state(\n cluster=cluster,\n containerInstances=[instance_arn], status='DRAINING')\n\n\ndef pick(dct, *keys):\n \"\"\"Pick a subset of a dict.\"\"\"\n return {k: v for k, v in dct.items() if k in keys}\n" }, - "Handler": "index.lambda_handler", "Role": { "Fn::GetAtt": [ "FargateClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole7FEDCD32", "Arn" ] }, - "Runtime": "python3.6", "Environment": { "Variables": { "CLUSTER": { @@ -345,6 +343,8 @@ } } }, + "Handler": "index.lambda_handler", + "Runtime": "python3.6", "Tags": [ { "Key": "Name", @@ -703,6 +703,12 @@ "StateMachine2E01A3A5": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", @@ -725,12 +731,6 @@ ":states:::ecs:runTask.sync\"}}}" ] ] - }, - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRoleB840431D", - "Arn" - ] } }, "DependsOn": [ @@ -745,4 +745,4 @@ "Default": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-task.expected.json index 7f15f160b7c39..00682997b2a31 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-task.expected.json @@ -252,6 +252,12 @@ "StateMachine2E01A3A5": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", @@ -281,12 +287,6 @@ ":states:::ecs:runTask.sync\"}}}" ] ] - }, - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRoleB840431D", - "Arn" - ] } }, "DependsOn": [ @@ -295,4 +295,4 @@ ] } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eks/integ.call.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eks/integ.call.expected.json index 23ad4160b9f7b..64b6481aab74f 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eks/integ.call.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eks/integ.call.expected.json @@ -1200,7 +1200,7 @@ }, "/", { - "Ref": "AssetParameters3aee2b76026cd725af3b14456bf03061e83d56cce0e0354c7c8e88ee1150e527S3Bucket7ED14FA7" + "Ref": "AssetParametersf14dbf982bfb4e61285bf9e6e91f20eb00e8a36c79c981ceec2437b42a02e216S3BucketE0529475" }, "/", { @@ -1210,7 +1210,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3aee2b76026cd725af3b14456bf03061e83d56cce0e0354c7c8e88ee1150e527S3VersionKeyF4EF0775" + "Ref": "AssetParametersf14dbf982bfb4e61285bf9e6e91f20eb00e8a36c79c981ceec2437b42a02e216S3VersionKey736B6614" } ] } @@ -1223,7 +1223,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters3aee2b76026cd725af3b14456bf03061e83d56cce0e0354c7c8e88ee1150e527S3VersionKeyF4EF0775" + "Ref": "AssetParametersf14dbf982bfb4e61285bf9e6e91f20eb00e8a36c79c981ceec2437b42a02e216S3VersionKey736B6614" } ] } @@ -1273,7 +1273,7 @@ }, "/", { - "Ref": "AssetParameterscad1ae036643e3cd17cd3b2b30a2f9e07c1aacaf5284314f41437e4c20447aeaS3BucketED16A657" + "Ref": "AssetParameters9d41118c36f364bd9f7de59dda09afbef182ac0f726496523af8ffd24aafc604S3BucketCAFDB26C" }, "/", { @@ -1283,7 +1283,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterscad1ae036643e3cd17cd3b2b30a2f9e07c1aacaf5284314f41437e4c20447aeaS3VersionKey37A80BBF" + "Ref": "AssetParameters9d41118c36f364bd9f7de59dda09afbef182ac0f726496523af8ffd24aafc604S3VersionKey3CF353E9" } ] } @@ -1296,7 +1296,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameterscad1ae036643e3cd17cd3b2b30a2f9e07c1aacaf5284314f41437e4c20447aeaS3VersionKey37A80BBF" + "Ref": "AssetParameters9d41118c36f364bd9f7de59dda09afbef182ac0f726496523af8ffd24aafc604S3VersionKey3CF353E9" } ] } @@ -1552,29 +1552,29 @@ "Type": "String", "Description": "Artifact hash for asset \"844c1a4b13479b359ea0e607dccb4a04b73e22cf88cf9b64feed2c5f0de213c0\"" }, - "AssetParameters3aee2b76026cd725af3b14456bf03061e83d56cce0e0354c7c8e88ee1150e527S3Bucket7ED14FA7": { + "AssetParametersf14dbf982bfb4e61285bf9e6e91f20eb00e8a36c79c981ceec2437b42a02e216S3BucketE0529475": { "Type": "String", - "Description": "S3 bucket for asset \"3aee2b76026cd725af3b14456bf03061e83d56cce0e0354c7c8e88ee1150e527\"" + "Description": "S3 bucket for asset \"f14dbf982bfb4e61285bf9e6e91f20eb00e8a36c79c981ceec2437b42a02e216\"" }, - "AssetParameters3aee2b76026cd725af3b14456bf03061e83d56cce0e0354c7c8e88ee1150e527S3VersionKeyF4EF0775": { + "AssetParametersf14dbf982bfb4e61285bf9e6e91f20eb00e8a36c79c981ceec2437b42a02e216S3VersionKey736B6614": { "Type": "String", - "Description": "S3 key for asset version \"3aee2b76026cd725af3b14456bf03061e83d56cce0e0354c7c8e88ee1150e527\"" + "Description": "S3 key for asset version \"f14dbf982bfb4e61285bf9e6e91f20eb00e8a36c79c981ceec2437b42a02e216\"" }, - "AssetParameters3aee2b76026cd725af3b14456bf03061e83d56cce0e0354c7c8e88ee1150e527ArtifactHash94EFED5E": { + "AssetParametersf14dbf982bfb4e61285bf9e6e91f20eb00e8a36c79c981ceec2437b42a02e216ArtifactHash85D00783": { "Type": "String", - "Description": "Artifact hash for asset \"3aee2b76026cd725af3b14456bf03061e83d56cce0e0354c7c8e88ee1150e527\"" + "Description": "Artifact hash for asset \"f14dbf982bfb4e61285bf9e6e91f20eb00e8a36c79c981ceec2437b42a02e216\"" }, - "AssetParameterscad1ae036643e3cd17cd3b2b30a2f9e07c1aacaf5284314f41437e4c20447aeaS3BucketED16A657": { + "AssetParameters9d41118c36f364bd9f7de59dda09afbef182ac0f726496523af8ffd24aafc604S3BucketCAFDB26C": { "Type": "String", - "Description": "S3 bucket for asset \"cad1ae036643e3cd17cd3b2b30a2f9e07c1aacaf5284314f41437e4c20447aea\"" + "Description": "S3 bucket for asset \"9d41118c36f364bd9f7de59dda09afbef182ac0f726496523af8ffd24aafc604\"" }, - "AssetParameterscad1ae036643e3cd17cd3b2b30a2f9e07c1aacaf5284314f41437e4c20447aeaS3VersionKey37A80BBF": { + "AssetParameters9d41118c36f364bd9f7de59dda09afbef182ac0f726496523af8ffd24aafc604S3VersionKey3CF353E9": { "Type": "String", - "Description": "S3 key for asset version \"cad1ae036643e3cd17cd3b2b30a2f9e07c1aacaf5284314f41437e4c20447aea\"" + "Description": "S3 key for asset version \"9d41118c36f364bd9f7de59dda09afbef182ac0f726496523af8ffd24aafc604\"" }, - "AssetParameterscad1ae036643e3cd17cd3b2b30a2f9e07c1aacaf5284314f41437e4c20447aeaArtifactHash11CEC9E5": { + "AssetParameters9d41118c36f364bd9f7de59dda09afbef182ac0f726496523af8ffd24aafc604ArtifactHashEC502675": { "Type": "String", - "Description": "Artifact hash for asset \"cad1ae036643e3cd17cd3b2b30a2f9e07c1aacaf5284314f41437e4c20447aea\"" + "Description": "Artifact hash for asset \"9d41118c36f364bd9f7de59dda09afbef182ac0f726496523af8ffd24aafc604\"" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts index 7a2aa196b3de2..92a451b908993 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/evaluate-expression.test.ts @@ -25,7 +25,7 @@ test('Eval with Node.js', () => { [ '{"StartAt":"Task","States":{"Task":{"End":true,"Type":"Task","Resource":"', { - 'Fn::GetAtt': ['Evala0d2ce44871b4e7487a1f5e63d7c3bdc4DAC06E1', 'Arn'], + 'Fn::GetAtt': ['Evalda2d1181604e4a4586941a6abd7fe42dF371675D', 'Arn'], }, '","Parameters":{"expression":"$.a + $.b","expressionAttributeValues":{"$.a.$":"$.a","$.b.$":"$.b"}}}}}', ], @@ -54,7 +54,7 @@ test('expression does not contain paths', () => { [ '{"StartAt":"Task","States":{"Task":{"End":true,"Type":"Task","Resource":"', { - 'Fn::GetAtt': ['Evala0d2ce44871b4e7487a1f5e63d7c3bdc4DAC06E1', 'Arn'], + 'Fn::GetAtt': ['Evalda2d1181604e4a4586941a6abd7fe42dF371675D', 'Arn'], }, '","Parameters":{"expression":"2 + 2","expressionAttributeValues":{}}}}}', ], @@ -79,7 +79,7 @@ test('with dash and underscore in path', () => { [ '{"StartAt":"Task","States":{"Task":{"End":true,"Type":"Task","Resource":"', { - 'Fn::GetAtt': ['Evala0d2ce44871b4e7487a1f5e63d7c3bdc4DAC06E1', 'Arn'], + 'Fn::GetAtt': ['Evalda2d1181604e4a4586941a6abd7fe42dF371675D', 'Arn'], }, '","Parameters":{"expression":"$.a_b + $.c-d + $[_e]","expressionAttributeValues":{"$.a_b.$":"$.a_b","$.c-d.$":"$.c-d","$[_e].$":"$[_e]"}}}}}', ], diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.glue-task.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.glue-task.expected.json index 8f9e9aba0102f..150ecb4c32161 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.glue-task.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.glue-task.expected.json @@ -233,6 +233,12 @@ "StateMachine81935E76": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRole543B9670", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", @@ -244,12 +250,6 @@ ":states:::glue:startJobRun.sync\"},\"End Task\":{\"Type\":\"Pass\",\"End\":true}}}" ] ] - }, - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRole543B9670", - "Arn" - ] } }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.expected.json index 1f916f6be06f1..217b47176d936 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.expected.json @@ -233,6 +233,12 @@ "StateMachine81935E76": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRole543B9670", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", @@ -244,12 +250,6 @@ ":states:::glue:startJobRun.sync\",\"Parameters\":{\"JobName\":\"My Glue Job\",\"Arguments\":{\"--enable-metrics\":\"true\"}}},\"End Task\":{\"Type\":\"Pass\",\"End\":true}}}" ] ] - }, - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRole543B9670", - "Arn" - ] } }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json index c48b04a826783..39a3502ba6c80 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.evaluate-expression.expected.json @@ -1,6 +1,6 @@ { "Resources": { - "Evala0d2ce44871b4e7487a1f5e63d7c3bdcServiceRoleDC85DDD3": { + "Evalda2d1181604e4a4586941a6abd7fe42dServiceRoleED144118": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -31,7 +31,7 @@ ] } }, - "Evala0d2ce44871b4e7487a1f5e63d7c3bdc4DAC06E1": { + "Evalda2d1181604e4a4586941a6abd7fe42dF371675D": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { @@ -74,7 +74,7 @@ }, "Role": { "Fn::GetAtt": [ - "Evala0d2ce44871b4e7487a1f5e63d7c3bdcServiceRoleDC85DDD3", + "Evalda2d1181604e4a4586941a6abd7fe42dServiceRoleED144118", "Arn" ] }, @@ -82,7 +82,7 @@ "Runtime": "nodejs14.x" }, "DependsOn": [ - "Evala0d2ce44871b4e7487a1f5e63d7c3bdcServiceRoleDC85DDD3" + "Evalda2d1181604e4a4586941a6abd7fe42dServiceRoleED144118" ] }, "StateMachineRoleB840431D": { @@ -123,7 +123,7 @@ "Effect": "Allow", "Resource": { "Fn::GetAtt": [ - "Evala0d2ce44871b4e7487a1f5e63d7c3bdc4DAC06E1", + "Evalda2d1181604e4a4586941a6abd7fe42dF371675D", "Arn" ] } @@ -155,21 +155,21 @@ "{\"StartAt\":\"Sum\",\"States\":{\"Sum\":{\"Next\":\"Multiply\",\"Type\":\"Task\",\"ResultPath\":\"$.c\",\"Resource\":\"", { "Fn::GetAtt": [ - "Evala0d2ce44871b4e7487a1f5e63d7c3bdc4DAC06E1", + "Evalda2d1181604e4a4586941a6abd7fe42dF371675D", "Arn" ] }, "\",\"Parameters\":{\"expression\":\"$.a + $.b\",\"expressionAttributeValues\":{\"$.a.$\":\"$.a\",\"$.b.$\":\"$.b\"}}},\"Multiply\":{\"Next\":\"Wait\",\"Type\":\"Task\",\"ResultPath\":\"$.d\",\"Resource\":\"", { "Fn::GetAtt": [ - "Evala0d2ce44871b4e7487a1f5e63d7c3bdc4DAC06E1", + "Evalda2d1181604e4a4586941a6abd7fe42dF371675D", "Arn" ] }, "\",\"Parameters\":{\"expression\":\"$.c * 2\",\"expressionAttributeValues\":{\"$.c.$\":\"$.c\"}}},\"Wait\":{\"Type\":\"Wait\",\"SecondsPath\":\"$.d\",\"Next\":\"Now\"},\"Now\":{\"End\":true,\"Type\":\"Task\",\"ResultPath\":\"$.now\",\"Resource\":\"", { "Fn::GetAtt": [ - "Evala0d2ce44871b4e7487a1f5e63d7c3bdc4DAC06E1", + "Evalda2d1181604e4a4586941a6abd7fe42dF371675D", "Arn" ] }, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.job-poller.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.job-poller.expected.json index 5087e1d80e41e..0a90d0093ec04 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.job-poller.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.job-poller.expected.json @@ -43,6 +43,12 @@ "StateMachine2E01A3A5": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", @@ -62,12 +68,6 @@ "\"}},\"TimeoutSeconds\":30}" ] ] - }, - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRoleB840431D", - "Arn" - ] } }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.start-execution.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.start-execution.expected.json index 40010a8634abe..7916ba084ade1 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.start-execution.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.start-execution.expected.json @@ -31,13 +31,13 @@ "ChildDAB30558": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { - "DefinitionString": "{\"StartAt\":\"Pass\",\"States\":{\"Pass\":{\"Type\":\"Pass\",\"End\":true}}}", "RoleArn": { "Fn::GetAtt": [ "ChildRole1E3E0EF5", "Arn" ] - } + }, + "DefinitionString": "{\"StartAt\":\"Pass\",\"States\":{\"Pass\":{\"Type\":\"Pass\",\"End\":true}}}" }, "DependsOn": [ "ChildRole1E3E0EF5" @@ -166,6 +166,12 @@ "Parent8B210403": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "ParentRole5F0C366C", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", @@ -181,12 +187,6 @@ ":states:::states:startExecution.sync\"}}}" ] ] - }, - "RoleArn": { - "Fn::GetAtt": [ - "ParentRole5F0C366C", - "Arn" - ] } }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke-function.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke-function.expected.json index 7306c708756c4..dec16cb9e6ba8 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke-function.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke-function.expected.json @@ -72,13 +72,13 @@ ] } }, - "Handler": "index.main", "Role": { "Fn::GetAtt": [ "HandlerServiceRoleFCDC14AE", "Arn" ] }, + "Handler": "index.main", "Runtime": "python3.6" }, "DependsOn": [ @@ -157,13 +157,13 @@ ] } }, - "Handler": "index.main", "Role": { "Fn::GetAtt": [ "CallbackHandlerServiceRole3689695E", "Arn" ] }, + "Handler": "index.main", "Runtime": "python3.6" }, "DependsOn": [ @@ -237,6 +237,12 @@ "StateMachine2E01A3A5": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", @@ -259,12 +265,6 @@ ":states:::lambda:invoke.waitForTaskToken\",\"ResultPath\":\"$.status\"},\"Job Complete?\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.status\",\"StringEquals\":\"FAILED\",\"Next\":\"Job Failed\"},{\"Variable\":\"$.status\",\"StringEquals\":\"SUCCEEDED\",\"Next\":\"Final step\"}]},\"Job Failed\":{\"Type\":\"Fail\",\"Error\":\"DescribeJob returned FAILED\",\"Cause\":\"AWS Batch Job Failed\"},\"Final step\":{\"Type\":\"Pass\",\"End\":true}},\"TimeoutSeconds\":30}" ] ] - }, - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRoleB840431D", - "Arn" - ] } }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.expected.json index efa3aac3cf73a..1d0e7fcdc6f51 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.expected.json @@ -37,13 +37,13 @@ "Code": { "ZipFile": "exports.handler = async () => {\n return {\n statusCode: '200',\n body: 'hello, world!'\n };\n };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "submitJobLambdaServiceRole4D897ABD", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -87,13 +87,13 @@ "Code": { "ZipFile": "exports.handler = async function(event, context) {\n return {\n status: event.statusCode === '200' ? 'SUCCEEDED' : 'FAILED'\n };\n };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "checkJobStateLambdaServiceRoleB8B57B65", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.payload.only.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.payload.only.expected.json index 32bec4cb4a2fe..d0a6cdda262dc 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.payload.only.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke.payload.only.expected.json @@ -37,13 +37,13 @@ "Code": { "ZipFile": "exports.handler = async () => {\n return {\n statusCode: '200',\n body: 'hello, world!'\n };\n };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "submitJobLambdaServiceRole4D897ABD", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -87,13 +87,13 @@ "Code": { "ZipFile": "exports.handler = async function(event, context) {\n return {\n status: event.statusCode === '200' ? 'SUCCEEDED' : 'FAILED'\n };\n };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "checkJobStateLambdaServiceRoleB8B57B65", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.run-lambda.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.run-lambda.expected.json index 64f8d2444d7f2..6c483349d059b 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.run-lambda.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.run-lambda.expected.json @@ -37,13 +37,13 @@ "Code": { "ZipFile": "exports.handler = async () => {\n return {\n statusCode: '200',\n body: 'hello, world!'\n };\n };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "submitJobLambdaServiceRole4D897ABD", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -87,13 +87,13 @@ "Code": { "ZipFile": "exports.handler = async function(event, context) {\n return {\n status: event.statusCode === '200' ? 'SUCCEEDED' : 'FAILED'\n };\n };" }, - "Handler": "index.handler", "Role": { "Fn::GetAtt": [ "checkJobStateLambdaServiceRoleB8B57B65", "Arn" ] }, + "Handler": "index.handler", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -167,6 +167,12 @@ "StateMachine2E01A3A5": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", @@ -190,12 +196,6 @@ ":states:::lambda:invoke\"},\"Job Complete?\":{\"Type\":\"Choice\",\"Choices\":[{\"Variable\":\"$.status\",\"StringEquals\":\"FAILED\",\"Next\":\"Job Failed\"},{\"Variable\":\"$.status\",\"StringEquals\":\"SUCCEEDED\",\"Next\":\"Final step\"}]},\"Job Failed\":{\"Type\":\"Fail\",\"Error\":\"Received a status that was not 200\",\"Cause\":\"Job Failed\"},\"Final step\":{\"Type\":\"Pass\",\"End\":true}},\"TimeoutSeconds\":30}" ] ] - }, - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRoleB840431D", - "Arn" - ] } }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json index c163ebd11cf77..107fd16780144 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.call-sagemaker.expected.json @@ -608,4 +608,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.expected.json index 209c5520c1c64..0cd3166d73138 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/integ.create-training-job.expected.json @@ -358,4 +358,4 @@ } } } -} +} \ No newline at end of file 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 index 7cd5dd9eed8f6..cd4621ee4c7f1 100644 --- 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 @@ -31,12 +31,17 @@ "ChildDAB30558": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { - "DefinitionString": "{\"StartAt\":\"Pass\",\"States\":{\"Pass\":{\"Type\":\"Pass\",\"End\":true}}}", "RoleArn": { - "Fn::GetAtt": ["ChildRole1E3E0EF5", "Arn"] - } + "Fn::GetAtt": [ + "ChildRole1E3E0EF5", + "Arn" + ] + }, + "DefinitionString": "{\"StartAt\":\"Pass\",\"States\":{\"Pass\":{\"Type\":\"Pass\",\"End\":true}}}" }, - "DependsOn": ["ChildRole1E3E0EF5"] + "DependsOn": [ + "ChildRole1E3E0EF5" + ] }, "ParentRole5F0C366C": { "Type": "AWS::IAM::Role", @@ -79,7 +84,10 @@ } }, { - "Action": ["states:DescribeExecution", "states:StopExecution"], + "Action": [ + "states:DescribeExecution", + "states:StopExecution" + ], "Effect": "Allow", "Resource": { "Fn::Join": [ @@ -117,7 +125,11 @@ } }, { - "Action": ["events:PutTargets", "events:PutRule", "events:DescribeRule"], + "Action": [ + "events:PutTargets", + "events:PutRule", + "events:DescribeRule" + ], "Effect": "Allow", "Resource": { "Fn::Join": [ @@ -154,6 +166,12 @@ "Parent8B210403": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "ParentRole5F0C366C", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", @@ -169,12 +187,12 @@ "\"}}}}" ] ] - }, - "RoleArn": { - "Fn::GetAtt": ["ParentRole5F0C366C", "Arn"] } }, - "DependsOn": ["ParentRoleDefaultPolicy9BDC56DC", "ParentRole5F0C366C"] + "DependsOn": [ + "ParentRoleDefaultPolicy9BDC56DC", + "ParentRole5F0C366C" + ] } }, "Outputs": { @@ -184,4 +202,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index 8682de8dd13a9..f22d45f9f475d 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -295,7 +295,7 @@ parallel.next(closeOrder); ### Succeed Reaching a `Succeed` state terminates the state machine execution with a -succesful status. +successful status. ```ts const success = new sfn.Succeed(this, 'We did it!'); diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/step-functions-task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/step-functions-task.ts index 1d0dfcecbf773..e722594345930 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/step-functions-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/step-functions-task.ts @@ -84,7 +84,7 @@ export interface StepFunctionsTaskConfig { * Three ways to call an integrated service: Request Response, Run a Job and Wait for a Callback with Task Token. * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html * - * Here, they are named as FIRE_AND_FORGET, SYNC and WAIT_FOR_TASK_TOKEN respectly. + * Here, they are named as FIRE_AND_FORGET, SYNC and WAIT_FOR_TASK_TOKEN respectfully. * * @default FIRE_AND_FORGET */ @@ -100,7 +100,7 @@ export enum ServiceIntegrationPattern { SYNC = 'SYNC', /** - * Call a service with a task token and wait until that token is returned by SendTaskSuccess/SendTaskFailure with paylaod + * Call a service with a task token and wait until that token is returned by SendTaskSuccess/SendTaskFailure with payload. */ WAIT_FOR_TASK_TOKEN = 'WAIT_FOR_TASK_TOKEN' } diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index 9485ec700294e..1affea5b1d444 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,62 @@ +# CloudFormation Resource Specification v30.1.0 + +## New Resource Types + +* AWS::Events::ApiDestination +* AWS::Events::Connection +* AWS::IoT::AccountAuditConfiguration +* AWS::IoT::CustomMetric +* AWS::IoT::Dimension +* AWS::IoT::MitigationAction +* AWS::IoT::ScheduledAudit +* AWS::IoT::SecurityProfile +* AWS::S3Outposts::AccessPoint +* AWS::S3Outposts::Bucket +* AWS::S3Outposts::BucketPolicy +* AWS::S3Outposts::Endpoint + +## Attribute Changes + +* AWS::Athena::WorkGroup EffectiveEngineVersion (__added__) + +## Property Changes + +* AWS::Backup::BackupVault BackupVaultTags.PrimitiveType (__deleted__) +* AWS::Backup::BackupVault BackupVaultTags.PrimitiveItemType (__added__) +* AWS::Backup::BackupVault BackupVaultTags.Type (__added__) +* AWS::Cloud9::EnvironmentEC2 ImageId (__added__) +* AWS::CloudFormation::ModuleVersion ModuleName.UpdateType (__changed__) + * Old: Mutable + * New: Immutable +* AWS::CloudFormation::ModuleVersion ModulePackage.Required (__changed__) + * Old: false + * New: true +* AWS::DataBrew::Dataset Format (__added__) +* AWS::Detective::MemberInvitation DisableEmailNotification (__added__) +* AWS::IVS::Channel RecordingConfigurationArn (__deleted__) +* AWS::SecretsManager::Secret ReplicaRegions (__added__) +* AWS::ServiceDiscovery::HttpNamespace Tags.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ServiceDiscovery::PrivateDnsNamespace Tags.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ServiceDiscovery::PublicDnsNamespace Tags.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ServiceDiscovery::Service Tags.UpdateType (__changed__) + * Old: Immutable + * New: Mutable + +## Property Type Changes + +* AWS::Athena::WorkGroup.EngineVersion (__added__) +* AWS::SecretsManager::Secret.ReplicaRegion (__added__) +* AWS::Athena::WorkGroup.WorkGroupConfiguration EngineVersion (__added__) +* AWS::Athena::WorkGroup.WorkGroupConfigurationUpdates EngineVersion (__added__) +* AWS::Backup::BackupVault.NotificationObjectType BackupVaultEvents.DuplicatesAllowed (__added__) + + # CloudFormation Resource Specification v30.0.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index 8dd5c17a1b5cd..a75ef34cbaa5c 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -30.0.0 +30.1.0 diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index 75da81b0c270b..05d46dca703b8 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -7164,6 +7164,23 @@ } } }, + "AWS::Athena::WorkGroup.EngineVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-athena-workgroup-engineversion.html", + "Properties": { + "EffectiveEngineVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-athena-workgroup-engineversion.html#cfn-athena-workgroup-engineversion-effectiveengineversion", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "SelectedEngineVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-athena-workgroup-engineversion.html#cfn-athena-workgroup-engineversion-selectedengineversion", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::Athena::WorkGroup.ResultConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-athena-workgroup-resultconfiguration.html", "Properties": { @@ -7225,6 +7242,12 @@ "Required": false, "UpdateType": "Mutable" }, + "EngineVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-athena-workgroup-workgroupconfiguration.html#cfn-athena-workgroup-workgroupconfiguration-engineversion", + "Required": false, + "Type": "EngineVersion", + "UpdateType": "Mutable" + }, "PublishCloudWatchMetricsEnabled": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-athena-workgroup-workgroupconfiguration.html#cfn-athena-workgroup-workgroupconfiguration-publishcloudwatchmetricsenabled", "PrimitiveType": "Boolean", @@ -7260,6 +7283,12 @@ "Required": false, "UpdateType": "Mutable" }, + "EngineVersion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-athena-workgroup-workgroupconfigurationupdates.html#cfn-athena-workgroup-workgroupconfigurationupdates-engineversion", + "Required": false, + "Type": "EngineVersion", + "UpdateType": "Mutable" + }, "PublishCloudWatchMetricsEnabled": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-athena-workgroup-workgroupconfigurationupdates.html#cfn-athena-workgroup-workgroupconfigurationupdates-publishcloudwatchmetricsenabled", "PrimitiveType": "Boolean", @@ -8376,6 +8405,7 @@ "Properties": { "BackupVaultEvents": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-backup-backupvault-notificationobjecttype.html#cfn-backup-backupvault-notificationobjecttype-backupvaultevents", + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": true, "Type": "List", @@ -27787,6 +27817,140 @@ } } }, + "AWS::IoT::AccountAuditConfiguration.AuditCheckConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfiguration.html", + "Properties": { + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfiguration.html#cfn-iot-accountauditconfiguration-auditcheckconfiguration-enabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::AccountAuditConfiguration.AuditCheckConfigurations": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html", + "Properties": { + "AuthenticatedCognitoRoleOverlyPermissiveCheck": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations-authenticatedcognitoroleoverlypermissivecheck", + "Required": false, + "Type": "AuditCheckConfiguration", + "UpdateType": "Mutable" + }, + "CaCertificateExpiringCheck": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations-cacertificateexpiringcheck", + "Required": false, + "Type": "AuditCheckConfiguration", + "UpdateType": "Mutable" + }, + "CaCertificateKeyQualityCheck": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations-cacertificatekeyqualitycheck", + "Required": false, + "Type": "AuditCheckConfiguration", + "UpdateType": "Mutable" + }, + "ConflictingClientIdsCheck": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations-conflictingclientidscheck", + "Required": false, + "Type": "AuditCheckConfiguration", + "UpdateType": "Mutable" + }, + "DeviceCertificateExpiringCheck": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations-devicecertificateexpiringcheck", + "Required": false, + "Type": "AuditCheckConfiguration", + "UpdateType": "Mutable" + }, + "DeviceCertificateKeyQualityCheck": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations-devicecertificatekeyqualitycheck", + "Required": false, + "Type": "AuditCheckConfiguration", + "UpdateType": "Mutable" + }, + "DeviceCertificateSharedCheck": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations-devicecertificatesharedcheck", + "Required": false, + "Type": "AuditCheckConfiguration", + "UpdateType": "Mutable" + }, + "IotPolicyOverlyPermissiveCheck": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations-iotpolicyoverlypermissivecheck", + "Required": false, + "Type": "AuditCheckConfiguration", + "UpdateType": "Mutable" + }, + "IotRoleAliasAllowsAccessToUnusedServicesCheck": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations-iotrolealiasallowsaccesstounusedservicescheck", + "Required": false, + "Type": "AuditCheckConfiguration", + "UpdateType": "Mutable" + }, + "IotRoleAliasOverlyPermissiveCheck": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations-iotrolealiasoverlypermissivecheck", + "Required": false, + "Type": "AuditCheckConfiguration", + "UpdateType": "Mutable" + }, + "LoggingDisabledCheck": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations-loggingdisabledcheck", + "Required": false, + "Type": "AuditCheckConfiguration", + "UpdateType": "Mutable" + }, + "RevokedCaCertificateStillActiveCheck": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations-revokedcacertificatestillactivecheck", + "Required": false, + "Type": "AuditCheckConfiguration", + "UpdateType": "Mutable" + }, + "RevokedDeviceCertificateStillActiveCheck": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations-revokeddevicecertificatestillactivecheck", + "Required": false, + "Type": "AuditCheckConfiguration", + "UpdateType": "Mutable" + }, + "UnauthenticatedCognitoRoleOverlyPermissiveCheck": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditcheckconfigurations.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations-unauthenticatedcognitoroleoverlypermissivecheck", + "Required": false, + "Type": "AuditCheckConfiguration", + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::AccountAuditConfiguration.AuditNotificationTarget": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditnotificationtarget.html", + "Properties": { + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditnotificationtarget.html#cfn-iot-accountauditconfiguration-auditnotificationtarget-enabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "RoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditnotificationtarget.html#cfn-iot-accountauditconfiguration-auditnotificationtarget-rolearn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "TargetArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditnotificationtarget.html#cfn-iot-accountauditconfiguration-auditnotificationtarget-targetarn", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::AccountAuditConfiguration.AuditNotificationTargetConfigurations": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditnotificationtargetconfigurations.html", + "Properties": { + "Sns": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-accountauditconfiguration-auditnotificationtargetconfigurations.html#cfn-iot-accountauditconfiguration-auditnotificationtargetconfigurations-sns", + "Required": false, + "Type": "AuditNotificationTarget", + "UpdateType": "Mutable" + } + } + }, "AWS::IoT::DomainConfiguration.AuthorizerConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-domainconfiguration-authorizerconfig.html", "Properties": { @@ -27827,6 +27991,127 @@ } } }, + "AWS::IoT::MitigationAction.ActionParams": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-actionparams.html", + "Properties": { + "AddThingsToThingGroupParams": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-actionparams.html#cfn-iot-mitigationaction-actionparams-addthingstothinggroupparams", + "Required": false, + "Type": "AddThingsToThingGroupParams", + "UpdateType": "Mutable" + }, + "EnableIoTLoggingParams": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-actionparams.html#cfn-iot-mitigationaction-actionparams-enableiotloggingparams", + "Required": false, + "Type": "EnableIoTLoggingParams", + "UpdateType": "Mutable" + }, + "PublishFindingToSnsParams": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-actionparams.html#cfn-iot-mitigationaction-actionparams-publishfindingtosnsparams", + "Required": false, + "Type": "PublishFindingToSnsParams", + "UpdateType": "Mutable" + }, + "ReplaceDefaultPolicyVersionParams": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-actionparams.html#cfn-iot-mitigationaction-actionparams-replacedefaultpolicyversionparams", + "Required": false, + "Type": "ReplaceDefaultPolicyVersionParams", + "UpdateType": "Mutable" + }, + "UpdateCACertificateParams": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-actionparams.html#cfn-iot-mitigationaction-actionparams-updatecacertificateparams", + "Required": false, + "Type": "UpdateCACertificateParams", + "UpdateType": "Mutable" + }, + "UpdateDeviceCertificateParams": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-actionparams.html#cfn-iot-mitigationaction-actionparams-updatedevicecertificateparams", + "Required": false, + "Type": "UpdateDeviceCertificateParams", + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::MitigationAction.AddThingsToThingGroupParams": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-addthingstothinggroupparams.html", + "Properties": { + "OverrideDynamicGroups": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-addthingstothinggroupparams.html#cfn-iot-mitigationaction-addthingstothinggroupparams-overridedynamicgroups", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "ThingGroupNames": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-addthingstothinggroupparams.html#cfn-iot-mitigationaction-addthingstothinggroupparams-thinggroupnames", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::MitigationAction.EnableIoTLoggingParams": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-enableiotloggingparams.html", + "Properties": { + "LogLevel": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-enableiotloggingparams.html#cfn-iot-mitigationaction-enableiotloggingparams-loglevel", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "RoleArnForLogging": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-enableiotloggingparams.html#cfn-iot-mitigationaction-enableiotloggingparams-rolearnforlogging", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::MitigationAction.PublishFindingToSnsParams": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-publishfindingtosnsparams.html", + "Properties": { + "TopicArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-publishfindingtosnsparams.html#cfn-iot-mitigationaction-publishfindingtosnsparams-topicarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::MitigationAction.ReplaceDefaultPolicyVersionParams": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-replacedefaultpolicyversionparams.html", + "Properties": { + "TemplateName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-replacedefaultpolicyversionparams.html#cfn-iot-mitigationaction-replacedefaultpolicyversionparams-templatename", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::MitigationAction.UpdateCACertificateParams": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-updatecacertificateparams.html", + "Properties": { + "Action": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-updatecacertificateparams.html#cfn-iot-mitigationaction-updatecacertificateparams-action", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::MitigationAction.UpdateDeviceCertificateParams": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-updatedevicecertificateparams.html", + "Properties": { + "Action": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-mitigationaction-updatedevicecertificateparams.html#cfn-iot-mitigationaction-updatedevicecertificateparams-action", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::IoT::ProvisioningTemplate.ProvisioningHook": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-provisioningtemplate-provisioninghook.html", "Properties": { @@ -27844,6 +28129,210 @@ } } }, + "AWS::IoT::SecurityProfile.AlertTarget": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-alerttarget.html", + "Properties": { + "AlertTargetArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-alerttarget.html#cfn-iot-securityprofile-alerttarget-alerttargetarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "RoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-alerttarget.html#cfn-iot-securityprofile-alerttarget-rolearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::SecurityProfile.Behavior": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-behavior.html", + "Properties": { + "Criteria": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-behavior.html#cfn-iot-securityprofile-behavior-criteria", + "Required": false, + "Type": "BehaviorCriteria", + "UpdateType": "Mutable" + }, + "Metric": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-behavior.html#cfn-iot-securityprofile-behavior-metric", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "MetricDimension": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-behavior.html#cfn-iot-securityprofile-behavior-metricdimension", + "Required": false, + "Type": "MetricDimension", + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-behavior.html#cfn-iot-securityprofile-behavior-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "SuppressAlerts": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-behavior.html#cfn-iot-securityprofile-behavior-suppressalerts", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::SecurityProfile.BehaviorCriteria": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-behaviorcriteria.html", + "Properties": { + "ComparisonOperator": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-behaviorcriteria.html#cfn-iot-securityprofile-behaviorcriteria-comparisonoperator", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ConsecutiveDatapointsToAlarm": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-behaviorcriteria.html#cfn-iot-securityprofile-behaviorcriteria-consecutivedatapointstoalarm", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "ConsecutiveDatapointsToClear": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-behaviorcriteria.html#cfn-iot-securityprofile-behaviorcriteria-consecutivedatapointstoclear", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "DurationSeconds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-behaviorcriteria.html#cfn-iot-securityprofile-behaviorcriteria-durationseconds", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "MlDetectionConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-behaviorcriteria.html#cfn-iot-securityprofile-behaviorcriteria-mldetectionconfig", + "Required": false, + "Type": "MachineLearningDetectionConfig", + "UpdateType": "Mutable" + }, + "StatisticalThreshold": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-behaviorcriteria.html#cfn-iot-securityprofile-behaviorcriteria-statisticalthreshold", + "Required": false, + "Type": "StatisticalThreshold", + "UpdateType": "Mutable" + }, + "Value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-behaviorcriteria.html#cfn-iot-securityprofile-behaviorcriteria-value", + "Required": false, + "Type": "MetricValue", + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::SecurityProfile.MachineLearningDetectionConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-machinelearningdetectionconfig.html", + "Properties": { + "ConfidenceLevel": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-machinelearningdetectionconfig.html#cfn-iot-securityprofile-machinelearningdetectionconfig-confidencelevel", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::SecurityProfile.MetricDimension": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-metricdimension.html", + "Properties": { + "DimensionName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-metricdimension.html#cfn-iot-securityprofile-metricdimension-dimensionname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Operator": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-metricdimension.html#cfn-iot-securityprofile-metricdimension-operator", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::SecurityProfile.MetricToRetain": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-metrictoretain.html", + "Properties": { + "Metric": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-metrictoretain.html#cfn-iot-securityprofile-metrictoretain-metric", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "MetricDimension": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-metrictoretain.html#cfn-iot-securityprofile-metrictoretain-metricdimension", + "Required": false, + "Type": "MetricDimension", + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::SecurityProfile.MetricValue": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-metricvalue.html", + "Properties": { + "Cidrs": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-metricvalue.html#cfn-iot-securityprofile-metricvalue-cidrs", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Count": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-metricvalue.html#cfn-iot-securityprofile-metricvalue-count", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Number": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-metricvalue.html#cfn-iot-securityprofile-metricvalue-number", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + }, + "Numbers": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-metricvalue.html#cfn-iot-securityprofile-metricvalue-numbers", + "DuplicatesAllowed": false, + "PrimitiveItemType": "Double", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Ports": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-metricvalue.html#cfn-iot-securityprofile-metricvalue-ports", + "DuplicatesAllowed": false, + "PrimitiveItemType": "Integer", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Strings": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-metricvalue.html#cfn-iot-securityprofile-metricvalue-strings", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::SecurityProfile.StatisticalThreshold": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-statisticalthreshold.html", + "Properties": { + "Statistic": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-securityprofile-statisticalthreshold.html#cfn-iot-securityprofile-statisticalthreshold-statistic", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::IoT::Thing.AttributePayload": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-thing-attributepayload.html", "Properties": { @@ -46954,6 +47443,93 @@ } } }, + "AWS::S3Outposts::AccessPoint.VpcConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-accesspoint-vpcconfiguration.html", + "Properties": { + "VpcId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-accesspoint-vpcconfiguration.html#cfn-s3outposts-accesspoint-vpcconfiguration-vpcid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, + "AWS::S3Outposts::Bucket.AbortIncompleteMultipartUpload": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-bucket-abortincompletemultipartupload.html", + "Properties": { + "DaysAfterInitiation": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-bucket-abortincompletemultipartupload.html#cfn-s3outposts-bucket-abortincompletemultipartupload-daysafterinitiation", + "PrimitiveType": "Integer", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::S3Outposts::Bucket.LifecycleConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-bucket-lifecycleconfiguration.html", + "Properties": { + "Rules": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-bucket-lifecycleconfiguration.html#cfn-s3outposts-bucket-lifecycleconfiguration-rules", + "DuplicatesAllowed": false, + "ItemType": "Rule", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::S3Outposts::Bucket.Rule": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-bucket-rule.html", + "Properties": { + "AbortIncompleteMultipartUpload": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-bucket-rule.html#cfn-s3outposts-bucket-rule-abortincompletemultipartupload", + "Required": false, + "Type": "AbortIncompleteMultipartUpload", + "UpdateType": "Mutable" + }, + "ExpirationDate": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-bucket-rule.html#cfn-s3outposts-bucket-rule-expirationdate", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "ExpirationInDays": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-bucket-rule.html#cfn-s3outposts-bucket-rule-expirationindays", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "Filter": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-bucket-rule.html#cfn-s3outposts-bucket-rule-filter", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" + }, + "Id": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-bucket-rule.html#cfn-s3outposts-bucket-rule-id", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Status": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-bucket-rule.html#cfn-s3outposts-bucket-rule-status", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::S3Outposts::Endpoint.NetworkInterface": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-endpoint-networkinterface.html", + "Properties": { + "NetworkInterfaceId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3outposts-endpoint-networkinterface.html#cfn-s3outposts-endpoint-networkinterface-networkinterfaceid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::SES::ConfigurationSetEventDestination.CloudWatchDestination": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ses-configurationseteventdestination-cloudwatchdestination.html", "Properties": { @@ -50395,6 +50971,23 @@ } } }, + "AWS::SecretsManager::Secret.ReplicaRegion": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-secret-replicaregion.html", + "Properties": { + "KmsKeyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-secret-replicaregion.html#cfn-secretsmanager-secret-replicaregion-kmskeyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Region": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-secretsmanager-secret-replicaregion.html#cfn-secretsmanager-secret-replicaregion-region", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::ServiceCatalog::CloudFormationProduct.ProvisioningArtifactProperties": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-servicecatalog-cloudformationproduct-provisioningartifactproperties.html", "Properties": { @@ -53056,7 +53649,7 @@ } } }, - "ResourceSpecificationVersion": "30.0.0", + "ResourceSpecificationVersion": "30.1.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -57096,6 +57689,9 @@ "Attributes": { "CreationTime": { "PrimitiveType": "String" + }, + "EffectiveEngineVersion": { + "PrimitiveType": "String" } }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-athena-workgroup.html", @@ -57810,8 +58406,9 @@ }, "BackupVaultTags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-backup-backupvault.html#cfn-backup-backupvault-backupvaulttags", - "PrimitiveType": "Json", + "PrimitiveItemType": "String", "Required": false, + "Type": "Map", "UpdateType": "Mutable" }, "EncryptionKeyArn": { @@ -58229,6 +58826,12 @@ "Required": false, "UpdateType": "Mutable" }, + "ImageId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloud9-environmentec2.html#cfn-cloud9-environmentec2-imageid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "InstanceType": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloud9-environmentec2.html#cfn-cloud9-environmentec2-instancetype", "PrimitiveType": "String", @@ -58372,12 +58975,12 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-moduleversion.html#cfn-cloudformation-moduleversion-modulename", "PrimitiveType": "String", "Required": true, - "UpdateType": "Mutable" + "UpdateType": "Immutable" }, "ModulePackage": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-moduleversion.html#cfn-cloudformation-moduleversion-modulepackage", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Immutable" } } @@ -61879,6 +62482,12 @@ "AWS::DataBrew::Dataset": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-databrew-dataset.html", "Properties": { + "Format": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-databrew-dataset.html#cfn-databrew-dataset-format", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "FormatOptions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-databrew-dataset.html#cfn-databrew-dataset-formatoptions", "PrimitiveType": "Json", @@ -62638,6 +63247,12 @@ "AWS::Detective::MemberInvitation": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-detective-memberinvitation.html", "Properties": { + "DisableEmailNotification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-detective-memberinvitation.html#cfn-detective-memberinvitation-disableemailnotification", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "GraphArn": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-detective-memberinvitation.html#cfn-detective-memberinvitation-grapharn", "PrimitiveType": "String", @@ -69094,6 +69709,52 @@ } } }, + "AWS::Events::ApiDestination": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-apidestination.html", + "Properties": { + "ConnectionArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-apidestination.html#cfn-events-apidestination-connectionarn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-apidestination.html#cfn-events-apidestination-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "HttpMethod": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-apidestination.html#cfn-events-apidestination-httpmethod", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "InvocationEndpoint": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-apidestination.html#cfn-events-apidestination-invocationendpoint", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "InvocationRateLimitPerSecond": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-apidestination.html#cfn-events-apidestination-invocationratelimitpersecond", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-apidestination.html#cfn-events-apidestination-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::Events::Archive": { "Attributes": { "ArchiveName": { @@ -69137,6 +69798,43 @@ } } }, + "AWS::Events::Connection": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "SecretArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-connection.html", + "Properties": { + "AuthParameters": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-connection.html#cfn-events-connection-authparameters", + "PrimitiveType": "Json", + "Required": true, + "UpdateType": "Mutable" + }, + "AuthorizationType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-connection.html#cfn-events-connection-authorizationtype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Description": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-connection.html#cfn-events-connection-description", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-events-connection.html#cfn-events-connection-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + } + } + }, "AWS::Events::EventBus": { "Attributes": { "Arn": { @@ -72094,12 +72792,6 @@ "Required": false, "UpdateType": "Mutable" }, - "RecordingConfigurationArn": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ivs-channel.html#cfn-ivs-channel-recordingconfigurationarn", - "PrimitiveType": "String", - "Required": false, - "UpdateType": "Mutable" - }, "Tags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ivs-channel.html#cfn-ivs-channel-tags", "DuplicatesAllowed": false, @@ -72847,6 +73539,35 @@ } } }, + "AWS::IoT::AccountAuditConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-accountauditconfiguration.html", + "Properties": { + "AccountId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-accountauditconfiguration.html#cfn-iot-accountauditconfiguration-accountid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "AuditCheckConfigurations": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-accountauditconfiguration.html#cfn-iot-accountauditconfiguration-auditcheckconfigurations", + "Required": true, + "Type": "AuditCheckConfigurations", + "UpdateType": "Mutable" + }, + "AuditNotificationTargetConfigurations": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-accountauditconfiguration.html#cfn-iot-accountauditconfiguration-auditnotificationtargetconfigurations", + "Required": false, + "Type": "AuditNotificationTargetConfigurations", + "UpdateType": "Mutable" + }, + "RoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-accountauditconfiguration.html#cfn-iot-accountauditconfiguration-rolearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::IoT::Authorizer": { "Attributes": { "Arn": { @@ -72944,6 +73665,80 @@ } } }, + "AWS::IoT::CustomMetric": { + "Attributes": { + "MetricArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-custommetric.html", + "Properties": { + "DisplayName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-custommetric.html#cfn-iot-custommetric-displayname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "MetricName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-custommetric.html#cfn-iot-custommetric-metricname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "MetricType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-custommetric.html#cfn-iot-custommetric-metrictype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-custommetric.html#cfn-iot-custommetric-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::Dimension": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-dimension.html", + "Properties": { + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-dimension.html#cfn-iot-dimension-name", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "StringValues": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-dimension.html#cfn-iot-dimension-stringvalues", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-dimension.html#cfn-iot-dimension-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-dimension.html#cfn-iot-dimension-type", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::IoT::DomainConfiguration": { "Attributes": { "Arn": { @@ -73011,6 +73806,45 @@ } } }, + "AWS::IoT::MitigationAction": { + "Attributes": { + "MitigationActionArn": { + "PrimitiveType": "String" + }, + "MitigationActionId": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-mitigationaction.html", + "Properties": { + "ActionName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-mitigationaction.html#cfn-iot-mitigationaction-actionname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "ActionParams": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-mitigationaction.html#cfn-iot-mitigationaction-actionparams", + "Required": true, + "Type": "ActionParams", + "UpdateType": "Mutable" + }, + "RoleArn": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-mitigationaction.html#cfn-iot-mitigationaction-rolearn", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-mitigationaction.html#cfn-iot-mitigationaction-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::IoT::Policy": { "Attributes": { "Arn": { @@ -73103,6 +73937,117 @@ } } }, + "AWS::IoT::ScheduledAudit": { + "Attributes": { + "ScheduledAuditArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-scheduledaudit.html", + "Properties": { + "DayOfMonth": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-scheduledaudit.html#cfn-iot-scheduledaudit-dayofmonth", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "DayOfWeek": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-scheduledaudit.html#cfn-iot-scheduledaudit-dayofweek", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Frequency": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-scheduledaudit.html#cfn-iot-scheduledaudit-frequency", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "ScheduledAuditName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-scheduledaudit.html#cfn-iot-scheduledaudit-scheduledauditname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-scheduledaudit.html#cfn-iot-scheduledaudit-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "TargetCheckNames": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-scheduledaudit.html#cfn-iot-scheduledaudit-targetchecknames", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::IoT::SecurityProfile": { + "Attributes": { + "SecurityProfileArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-securityprofile.html", + "Properties": { + "AdditionalMetricsToRetainV2": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-securityprofile.html#cfn-iot-securityprofile-additionalmetricstoretainv2", + "DuplicatesAllowed": false, + "ItemType": "MetricToRetain", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "AlertTargets": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-securityprofile.html#cfn-iot-securityprofile-alerttargets", + "ItemType": "AlertTarget", + "Required": false, + "Type": "Map", + "UpdateType": "Mutable" + }, + "Behaviors": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-securityprofile.html#cfn-iot-securityprofile-behaviors", + "DuplicatesAllowed": false, + "ItemType": "Behavior", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "SecurityProfileDescription": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-securityprofile.html#cfn-iot-securityprofile-securityprofiledescription", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "SecurityProfileName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-securityprofile.html#cfn-iot-securityprofile-securityprofilename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-securityprofile.html#cfn-iot-securityprofile-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "TargetArns": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-securityprofile.html#cfn-iot-securityprofile-targetarns", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::IoT::Thing": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-thing.html", "Properties": { @@ -81983,6 +82928,138 @@ } } }, + "AWS::S3Outposts::AccessPoint": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-accesspoint.html", + "Properties": { + "Bucket": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-accesspoint.html#cfn-s3outposts-accesspoint-bucket", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-accesspoint.html#cfn-s3outposts-accesspoint-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Policy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-accesspoint.html#cfn-s3outposts-accesspoint-policy", + "PrimitiveType": "Json", + "Required": false, + "UpdateType": "Mutable" + }, + "VpcConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-accesspoint.html#cfn-s3outposts-accesspoint-vpcconfiguration", + "Required": true, + "Type": "VpcConfiguration", + "UpdateType": "Immutable" + } + } + }, + "AWS::S3Outposts::Bucket": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-bucket.html", + "Properties": { + "BucketName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-bucket.html#cfn-s3outposts-bucket-bucketname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "LifecycleConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-bucket.html#cfn-s3outposts-bucket-lifecycleconfiguration", + "Required": false, + "Type": "LifecycleConfiguration", + "UpdateType": "Mutable" + }, + "OutpostId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-bucket.html#cfn-s3outposts-bucket-outpostid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-bucket.html#cfn-s3outposts-bucket-tags", + "DuplicatesAllowed": false, + "ItemType": "Tag", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, + "AWS::S3Outposts::BucketPolicy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-bucketpolicy.html", + "Properties": { + "Bucket": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-bucketpolicy.html#cfn-s3outposts-bucketpolicy-bucket", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "PolicyDocument": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-bucketpolicy.html#cfn-s3outposts-bucketpolicy-policydocument", + "PrimitiveType": "Json", + "Required": true, + "UpdateType": "Mutable" + } + } + }, + "AWS::S3Outposts::Endpoint": { + "Attributes": { + "Arn": { + "PrimitiveType": "String" + }, + "CidrBlock": { + "PrimitiveType": "String" + }, + "CreationTime": { + "PrimitiveType": "String" + }, + "Id": { + "PrimitiveType": "String" + }, + "NetworkInterfaces": { + "DuplicatesAllowed": false, + "ItemType": "NetworkInterface", + "Type": "List" + }, + "Status": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-endpoint.html", + "Properties": { + "OutpostId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-endpoint.html#cfn-s3outposts-endpoint-outpostid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "SecurityGroupId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-endpoint.html#cfn-s3outposts-endpoint-securitygroupid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "SubnetId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-s3outposts-endpoint.html#cfn-s3outposts-endpoint-subnetid", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, "AWS::SDB::Domain": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-simpledb.html", "Properties": { @@ -84282,6 +85359,13 @@ "Required": false, "UpdateType": "Immutable" }, + "ReplicaRegions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secret.html#cfn-secretsmanager-secret-replicaregions", + "ItemType": "ReplicaRegion", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "SecretString": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secret.html#cfn-secretsmanager-secret-secretstring", "PrimitiveType": "String", @@ -85124,7 +86208,7 @@ "ItemType": "Tag", "Required": false, "Type": "List", - "UpdateType": "Immutable" + "UpdateType": "Mutable" } } }, @@ -85179,7 +86263,7 @@ "ItemType": "Tag", "Required": false, "Type": "List", - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Vpc": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-privatednsnamespace.html#cfn-servicediscovery-privatednsnamespace-vpc", @@ -85217,7 +86301,7 @@ "ItemType": "Tag", "Required": false, "Type": "List", - "UpdateType": "Immutable" + "UpdateType": "Mutable" } } }, @@ -85276,7 +86360,7 @@ "ItemType": "Tag", "Required": false, "Type": "List", - "UpdateType": "Immutable" + "UpdateType": "Mutable" } } }, diff --git a/packages/@aws-cdk/cloudformation-include/build.js b/packages/@aws-cdk/cloudformation-include/build.js index ab36ffd345d7f..454a10a5014d5 100644 --- a/packages/@aws-cdk/cloudformation-include/build.js +++ b/packages/@aws-cdk/cloudformation-include/build.js @@ -29,6 +29,8 @@ async function main() { for (const constructLibraryDir of constructLibrariesDirs) { const absConstructLibraryDir = path.resolve(constructLibrariesRoot, constructLibraryDir); + if (!fs.statSync(absConstructLibraryDir).isDirectory()) { continue; } // .DS_Store + const libraryPackageJson = require(path.join(absConstructLibraryDir, 'package.json')); const libraryDependencyVersion = dependencies[libraryPackageJson.name]; diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index fc875f1a192b2..fd17167302be3 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -187,6 +187,7 @@ "@aws-cdk/aws-route53": "0.0.0", "@aws-cdk/aws-route53resolver": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3outposts": "0.0.0", "@aws-cdk/aws-sagemaker": "0.0.0", "@aws-cdk/aws-sam": "0.0.0", "@aws-cdk/aws-sdb": "0.0.0", @@ -335,6 +336,7 @@ "@aws-cdk/aws-route53": "0.0.0", "@aws-cdk/aws-route53resolver": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3outposts": "0.0.0", "@aws-cdk/aws-sagemaker": "0.0.0", "@aws-cdk/aws-sam": "0.0.0", "@aws-cdk/aws-sdb": "0.0.0", diff --git a/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts b/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts index fe87b644761d6..dc29f08713133 100644 --- a/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts +++ b/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts @@ -1,5 +1,7 @@ import { Construct } from 'constructs'; +import { CustomResource } from '../custom-resource'; import { CustomResourceProvider, CustomResourceProviderRuntime } from '../custom-resource-provider'; +import { CfnUtilsResourceType } from './cfn-utils-provider/consts'; /** * A custom resource provider for CFN utilities such as `CfnJson`. @@ -11,4 +13,35 @@ export class CfnUtilsProvider extends Construct { codeDirectory: `${__dirname}/cfn-utils-provider`, }); } +} + +/** + * Utility functions provided by the CfnUtilsProvider + */ +export abstract class CfnUtils { + /** + * Encode a structure to JSON at CloudFormation deployment time + * + * This would have been suitable for the JSON-encoding of abitrary structures, however: + * + * - It uses a custom resource to do the encoding, and we'd rather not use a custom + * resource if we can avoid it. + * - It cannot be used to encode objects where the keys of the objects can contain + * tokens--because those cannot be represented in the JSON encoding that CloudFormation + * templates use. + * + * This helper is used by `CloudFormationLang.toJSON()` if and only if it encounters + * objects that cannot be stringified any other way. + */ + public static stringify(scope: Construct, id: string, value: any): string { + const resource = new CustomResource(scope, id, { + serviceToken: CfnUtilsProvider.getOrCreate(scope), + resourceType: CfnUtilsResourceType.CFN_JSON_STRINGIFY, + properties: { + Value: value, + }, + }); + + return resource.getAttString('Value'); + } } \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/private/cfn-utils-provider/consts.ts b/packages/@aws-cdk/core/lib/private/cfn-utils-provider/consts.ts index b1571cabd5b42..9718dcef40645 100644 --- a/packages/@aws-cdk/core/lib/private/cfn-utils-provider/consts.ts +++ b/packages/@aws-cdk/core/lib/private/cfn-utils-provider/consts.ts @@ -5,5 +5,10 @@ export const enum CfnUtilsResourceType { /** * CfnJson */ - CFN_JSON = 'Custom::AWSCDKCfnJson' + CFN_JSON = 'Custom::AWSCDKCfnJson', + + /** + * CfnJsonStringify + */ + CFN_JSON_STRINGIFY = 'Custom::AWSCDKCfnJsonStringify', } diff --git a/packages/@aws-cdk/core/lib/private/cfn-utils-provider/index.ts b/packages/@aws-cdk/core/lib/private/cfn-utils-provider/index.ts index 87bd6bb070e16..f082001f80159 100644 --- a/packages/@aws-cdk/core/lib/private/cfn-utils-provider/index.ts +++ b/packages/@aws-cdk/core/lib/private/cfn-utils-provider/index.ts @@ -9,6 +9,9 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent if (event.ResourceType === CfnUtilsResourceType.CFN_JSON) { return cfnJsonHandler(event); } + if (event.ResourceType === CfnUtilsResourceType.CFN_JSON_STRINGIFY) { + return cfnJsonStringifyHandler(event); + } throw new Error(`unexpected resource type "${event.ResourceType}`); } @@ -20,3 +23,11 @@ function cfnJsonHandler(event: AWSLambda.CloudFormationCustomResourceEvent) { }, }; } + +function cfnJsonStringifyHandler(event: AWSLambda.CloudFormationCustomResourceEvent) { + return { + Data: { + Value: JSON.stringify(event.ResourceProperties.Value), + }, + }; +} diff --git a/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts b/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts index 4a74665b8f338..c2be580474426 100644 --- a/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts +++ b/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts @@ -1,10 +1,9 @@ import { Lazy } from '../lazy'; -import { Reference } from '../reference'; -import { DefaultTokenResolver, IFragmentConcatenator, IPostProcessor, IResolvable, IResolveContext } from '../resolvable'; -import { TokenizedStringFragments } from '../string-fragments'; +import { DefaultTokenResolver, IFragmentConcatenator, IResolveContext } from '../resolvable'; +import { Stack } from '../stack'; import { Token } from '../token'; -import { Intrinsic } from './intrinsic'; -import { resolve } from './resolve'; +import { CfnUtils } from './cfn-utils-provider'; +import { INTRINSIC_KEY_PREFIX, ResolutionTypeHint, resolvedTypeHint } from './resolve'; /** * Routines that know how to do operations at the CloudFormation document language level @@ -24,59 +23,12 @@ export class CloudFormationLang { * @param space Indentation to use (default: no pretty-printing) */ public static toJSON(obj: any, space?: number): string { - // This works in two stages: - // - // First, resolve everything. This gets rid of the lazy evaluations, evaluation - // to the real types of things (for example, would a function return a string, an - // intrinsic, or a number? We have to resolve to know). - // - // We then to through the returned result, identify things that evaluated to - // CloudFormation intrinsics, and re-wrap those in Tokens that have a - // toJSON() method returning their string representation. If we then call - // JSON.stringify() on that result, that gives us essentially the same - // string that we started with, except with the non-token characters quoted. - // - // {"field": "${TOKEN}"} --> {\"field\": \"${TOKEN}\"} - // - // A final resolve() on that string (done by the framework) will yield the string - // we're after. - // - // Resolving and wrapping are done in go using the resolver framework. - class IntrinsincWrapper extends DefaultTokenResolver { - constructor() { - super(CLOUDFORMATION_CONCAT); - } - - public resolveToken(t: IResolvable, context: IResolveContext, postProcess: IPostProcessor) { - // Return References directly, so their type is maintained and the references will - // continue to work. Only while preparing, because we do need the final value of the - // token while resolving. - if (Reference.isReference(t) && context.preparing) { return wrap(t); } - - // Deep-resolve and wrap. This is necessary for Lazy tokens so we can see "inside" them. - return wrap(super.resolveToken(t, context, postProcess)); - } - public resolveString(fragments: TokenizedStringFragments, context: IResolveContext) { - return wrap(super.resolveString(fragments, context)); - } - public resolveList(l: string[], context: IResolveContext) { - return wrap(super.resolveList(l, context)); - } - } - - // We need a ResolveContext to get started so return a Token - return Lazy.stringValue({ - produce: (ctx: IResolveContext) => - JSON.stringify(resolve(obj, { - preparing: ctx.preparing, - scope: ctx.scope, - resolver: new IntrinsincWrapper(), - }), undefined, space), + return Lazy.uncachedString({ + // We used to do this by hooking into `JSON.stringify()` by adding in objects + // with custom `toJSON()` functions, but it's ultimately simpler just to + // reimplement the `stringify()` function from scratch. + produce: (ctx) => tokenAwareStringify(obj, space ?? 0, ctx), }); - - function wrap(value: any): any { - return isIntrinsic(value) ? new JsonToken(deepQuoteStringsForJSON(value)) : value; - } } /** @@ -97,44 +49,250 @@ export class CloudFormationLang { // Otherwise return a Join intrinsic (already in the target document language to avoid taking // circular dependencies on FnJoin & friends) - return { 'Fn::Join': ['', minimalCloudFormationJoin('', parts)] }; + return fnJoinConcat(parts); } } /** - * Token that also stringifies in the toJSON() operation. + * Return a CFN intrinsic mass concatting any number of CloudFormation expressions */ -class JsonToken extends Intrinsic { - /** - * Special handler that gets called when JSON.stringify() is used. - */ - public toJSON() { - return this.toString(); - } +function fnJoinConcat(parts: any[]) { + return { 'Fn::Join': ['', minimalCloudFormationJoin('', parts)] }; } /** - * Deep escape strings for use in a JSON context + * Perform a JSON.stringify()-like operation, except aware of Tokens and CloudFormation intrincics + * + * Tokens will be resolved and if any resolve to CloudFormation intrinsics, the intrinsics + * will be lifted to the top of a giant `{ Fn::Join }` expression. + * + * If Tokens resolve to primitive types (for example, by using Lazies), we'll + * use the primitive type to determine how to encode the value into the JSON. + * + * If Tokens resolve to CloudFormation intrinsics, we'll use the type of the encoded + * value as a type hint to determine how to encode the value into the JSON. The difference + * is that we add quotes (") around strings, and don't add anything around non-strings. + * + * The following structure: + * + * { SomeAttr: resource.someAttr } + * + * Will JSONify to either: + * + * '{ "SomeAttr": "' ++ { Fn::GetAtt: [Resource, SomeAttr] } ++ '" }' + * or '{ "SomeAttr": ' ++ { Fn::GetAtt: [Resource, SomeAttr] } ++ ' }' + * + * Depending on whether `someAttr` is type-hinted to be a string or not. + * + * (Where ++ is the CloudFormation string-concat operation (`{ Fn::Join }`). + * + * ----------------------- + * + * This work requires 2 features from the `resolve()` function: + * + * - INTRINSICS TYPE HINTS: intrinsics are represented by values like + * `{ Ref: 'XYZ' }`. These values can reference either a string or a list/number at + * deploy time, and from the value alone there's no way to know which. We need + * to know the type to know whether to JSONify this reference to: + * + * '{ "referencedValue": "' ++ { Ref: XYZ } ++ '"}' + * or '{ "referencedValue": ' ++ { Ref: XYZ } ++ '}' + * + * I.e., whether or not we need to enclose the reference in quotes or not. + * + * We COULD have done this by resolving one token at a time, and looking at the + * type of the encoded token we were resolving to obtain a type hint. However, + * the `resolve()` and Token system resist a level-at-a-time resolve + * operation: because of the existence of post-processors, we must have done a + * complete recursive resolution of a token before we can look at its result + * (after which any type information about the sources of nested resolved + * values is lost). + * + * To fix this, "type hints" have been added to the `resolve()` function, + * giving an idea of the type of the source value for compplex result values. + * This only works for objects (not strings and numbers) but fortunately + * we only care about the types of intrinsics, which are always complex values. + * + * Type hinting could have been added to the `IResolvable` protocol as well, + * but for now we just use the type of an encoded value as a type hint. That way + * we don't need to annotate anything more at the L1 level--we will use the type + * encodings added by construct authors at the L2 levels. L1 users can escape the + * default decision of "string" by using `Token.asList()`. + * + * - COMPLEX KEYS: since tokens can be string-encoded, we can use string-encoded tokens + * as the keys in JavaScript objects. However, after resolution, those string-encoded + * tokens could resolve to intrinsics (`{ Ref: ... }`), which CANNOT be stored in + * JavaScript objects anymore. + * + * We therefore need a protocol to store the resolved values somewhere in the JavaScript + * type model, which can be returned by `resolve()`, and interpreted by `tokenAwareStringify()` + * to produce the correct JSON. + * + * And example will quickly show the point: + * + * User writes: + * { [resource.resourceName]: 'SomeValue' } + * ------ string actually looks like ------> + * { '${Token[1234]}': 'SomeValue' } + * ------ resolve -------> + * { '$IntrinsicKey$0': [ {Ref: Resource}, 'SomeValue' ] } + * ------ tokenAwareStringify -------> + * '{ "' ++ { Ref: Resource } ++ '": "SomeValue" }' */ -function deepQuoteStringsForJSON(x: any): any { - if (typeof x === 'string') { - // Whenever we escape a string we strip off the outermost quotes - // since we're already in a quoted context. - const stringified = JSON.stringify(x); - return stringified.substring(1, stringified.length - 1); +function tokenAwareStringify(root: any, space: number, ctx: IResolveContext) { + let indent = 0; + + const ret = new Array(); + + // First completely resolve the tree, then encode to JSON while respecting the type + // hints we got for the resolved intrinsics. + recurse(ctx.resolve(root, { allowIntrinsicKeys: true })); + + switch (ret.length) { + case 0: return undefined; + case 1: return renderSegment(ret[0]); + default: + return fnJoinConcat(ret.map(renderSegment)); } - if (Array.isArray(x)) { - return x.map(deepQuoteStringsForJSON); + /** + * Stringify a JSON element + */ + function recurse(obj: any): void { + if (obj === undefined) { return; } + + if (Token.isUnresolved(obj)) { + throw new Error('This shouldnt happen anymore'); + } + if (Array.isArray(obj)) { + return renderCollection('[', ']', obj, recurse); + } + if (typeof obj === 'object' && obj != null && !(obj instanceof Date)) { + // Treat as an intrinsic if this LOOKS like a CFN intrinsic (`{ Ref: ... }`) + // AND it's the result of a token resolution. Otherwise, we just treat this + // value as a regular old JSON object (that happens to look a lot like an intrinsic). + if (isIntrinsic(obj) && resolvedTypeHint(obj)) { + renderIntrinsic(obj); + return; + } + + return renderCollection('{', '}', definedEntries(obj), ([key, value]) => { + if (key.startsWith(INTRINSIC_KEY_PREFIX)) { + [key, value] = value; + } + + recurse(key); + pushLiteral(prettyPunctuation(':')); + recurse(value); + }); + } + // Otherwise we have a scalar, defer to JSON.stringify()s serialization + pushLiteral(JSON.stringify(obj)); } - if (typeof x === 'object') { - for (const key of Object.keys(x)) { - x[key] = deepQuoteStringsForJSON(x[key]); + /** + * Render an object or list + */ + function renderCollection(pre: string, post: string, xs: Iterable, each: (x: A) => void) { + pushLiteral(pre); + indent += space; + let atLeastOne = false; + for (const [comma, item] of sepIter(xs)) { + if (comma) { pushLiteral(','); } + pushLineBreak(); + each(item); + atLeastOne = true; } + indent -= space; + if (atLeastOne) { pushLineBreak(); } + pushLiteral(post); } - return x; + function renderIntrinsic(intrinsic: any) { + switch (resolvedTypeHint(intrinsic)) { + case ResolutionTypeHint.STRING: + pushLiteral('"'); + pushIntrinsic(deepQuoteStringLiterals(intrinsic)); + pushLiteral('"'); + return; + + case ResolutionTypeHint.LIST: + // We need this to look like: + // + // '{"listValue":' ++ STRINGIFY(CFN_EVAL({ Ref: MyList })) ++ '}' + // + // However, STRINGIFY would need to execute at CloudFormation deployment time, and that doesn't exist. + // + // We could *ALMOST* use: + // + // '{"listValue":["' ++ JOIN('","', { Ref: MyList }) ++ '"]}' + // + // But that has the unfortunate side effect that if `CFN_EVAL({ Ref: MyList }) == []`, then it would + // evaluate to `[""]`, which is a different value. Since CloudFormation does not have arbitrary + // conditionals there's no way to deal with this case properly. + // + // Therefore, if we encounter lists we need to defer to a custom resource to handle + // them properly at deploy time. + pushIntrinsic(CfnUtils.stringify(Stack.of(ctx.scope), `CdkJsonStringify${stringifyCounter++}`, intrinsic)); + return; + + case ResolutionTypeHint.NUMBER: + pushIntrinsic(intrinsic); + return; + } + + throw new Error(`Unexpected type hint: ${resolvedTypeHint(intrinsic)}`); + } + + /** + * Push a literal onto the current segment if it's also a literal, otherwise open a new Segment + */ + function pushLiteral(lit: string) { + let last = ret[ret.length - 1]; + if (last?.type !== 'literal') { + last = { type: 'literal', parts: [] }; + ret.push(last); + } + last.parts.push(lit); + } + + /** + * Add a new intrinsic segment + */ + function pushIntrinsic(intrinsic: any) { + ret.push({ type: 'intrinsic', intrinsic }); + } + + /** + * Push a line break if we are pretty-printing, otherwise don't + */ + function pushLineBreak() { + if (space > 0) { + pushLiteral(`\n${' '.repeat(indent)}`); + } + } + + /** + * Add a space after the punctuation if we are pretty-printing, no space if not + */ + function prettyPunctuation(punc: string) { + return space > 0 ? `${punc} ` : punc; + } +} + +/** + * A Segment is either a literal string or a CloudFormation intrinsic + */ +type Segment = { type: 'literal'; parts: string[] } | { type: 'intrinsic'; intrinsic: any }; + +/** + * Render a segment + */ +function renderSegment(s: Segment): NonNullable { + switch (s.type) { + case 'literal': return s.parts.join(''); + case 'intrinsic': return s.intrinsic; + } } const CLOUDFORMATION_CONCAT: IFragmentConcatenator = { @@ -204,3 +362,60 @@ export function isNameOfCloudFormationIntrinsic(name: string): boolean { // these are 'fake' intrinsics, only usable inside the parameter overrides of a CFN CodePipeline Action return name !== 'Fn::GetArtifactAtt' && name !== 'Fn::GetParam'; } + +/** + * Separated iterator + */ +function* sepIter(xs: Iterable): IterableIterator<[boolean, A]> { + let comma = false; + for (const item of xs) { + yield [comma, item]; + comma = true; + } +} + +/** + * Object.entries() but skipping undefined values + */ +function* definedEntries(xs: A): IterableIterator<[string, any]> { + for (const [key, value] of Object.entries(xs)) { + if (value !== undefined) { + yield [key, value]; + } + } +} + +/** + * Quote string literals inside an intrinsic + * + * Formally, this should only match string literals that will be interpreted as + * string literals. Fortunately, the strings that should NOT be quoted are + * Logical IDs and attribute names, which cannot contain quotes anyway. Hence, + * we can get away not caring about the distinction and just quoting everything. + */ +function deepQuoteStringLiterals(x: any): any { + if (Array.isArray(x)) { + return x.map(deepQuoteStringLiterals); + } + if (typeof x === 'object' && x != null) { + const ret: any = {}; + for (const [key, value] of Object.entries(x)) { + ret[deepQuoteStringLiterals(key)] = deepQuoteStringLiterals(value); + } + return ret; + } + if (typeof x === 'string') { + return quoteString(x); + } + return x; +} + +/** + * Quote the characters inside a string, for use inside toJSON + */ +function quoteString(s: string) { + s = JSON.stringify(s); + return s.substring(1, s.length - 1); +} + +let stringifyCounter = 1; \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/private/metadata-resource.ts b/packages/@aws-cdk/core/lib/private/metadata-resource.ts index 53b8217a81f88..17818add1a3ee 100644 --- a/packages/@aws-cdk/core/lib/private/metadata-resource.ts +++ b/packages/@aws-cdk/core/lib/private/metadata-resource.ts @@ -1,4 +1,4 @@ -import * as cxapi from '@aws-cdk/cx-api'; +import * as zlib from 'zlib'; import { RegionInfo } from '@aws-cdk/region-info'; import { Construct } from 'constructs'; import { CfnCondition } from '../cfn-condition'; @@ -8,41 +8,12 @@ import { CfnResource } from '../cfn-resource'; import { Lazy } from '../lazy'; import { Stack } from '../stack'; import { Token } from '../token'; -import { collectRuntimeInformation } from './runtime-info'; +import { ConstructInfo, constructInfoFromStack } from './runtime-info'; /** * Construct that will render the metadata resource */ export class MetadataResource extends Construct { - /** - * Clear the modules cache - * - * The next time the MetadataResource is rendered, it will do a lookup of the - * modules from the NodeJS module cache again. - * - * Used only for unit tests. - */ - public static clearModulesCache() { - this._modulesPropertyCache = undefined; - } - - /** - * Cached version of the _modulesProperty() accessor - * - * No point in calculating this fairly expensive list more than once. - */ - private static _modulesPropertyCache?: string; - - /** - * Calculate the modules property - */ - private static modulesProperty(): string { - if (this._modulesPropertyCache === undefined) { - this._modulesPropertyCache = formatModules(collectRuntimeInformation()); - } - return this._modulesPropertyCache; - } - constructor(scope: Stack, id: string) { super(scope, id); @@ -51,7 +22,7 @@ export class MetadataResource extends Construct { const resource = new CfnResource(this, 'Default', { type: 'AWS::CDK::Metadata', properties: { - Modules: Lazy.string({ produce: () => MetadataResource.modulesProperty() }), + Analytics: Lazy.string({ produce: () => formatAnalytics(constructInfoFromStack(scope)) }), }, }); @@ -76,17 +47,81 @@ function makeCdkMetadataAvailableCondition() { .map(ri => Fn.conditionEquals(Aws.REGION, ri.name))); } -function formatModules(runtime: cxapi.RuntimeInfo): string { - const modules = new Array(); +/** Convenience type for arbitrarily-nested map */ +class Trie extends Map { } - // inject toolkit version to list of modules - const cliVersion = process.env[cxapi.CLI_VERSION_ENV]; - if (cliVersion) { - modules.push(`aws-cdk=${cliVersion}`); - } +/** + * Formats a list of construct fully-qualified names (FQNs) and versions into a (possibly compressed) prefix-encoded string. + * + * The list of ConstructInfos is logically formatted into: + * ${version}!${fqn} (e.g., "1.90.0!aws-cdk-lib.Stack") + * and then all of the construct-versions are grouped with common prefixes together, grouping common parts in '{}' and separating items with ','. + * + * Example: + * [1.90.0!aws-cdk-lib.Stack, 1.90.0!aws-cdk-lib.Construct, 1.90.0!aws-cdk-lib.service.Resource, 0.42.1!aws-cdk-lib-experiments.NewStuff] + * Becomes: + * 1.90.0!aws-cdk-lib.{Stack,Construct,service.Resource},0.42.1!aws-cdk-lib-experiments.NewStuff + * + * The whole thing is then either included directly as plaintext as: + * v2:plaintext:{prefixEncodedList} + * Or is compressed and base64-encoded, and then formatted as: + * v2:deflate64:{prefixEncodedListCompressedAndEncoded} + * + * Exported/visible for ease of testing. + */ +export function formatAnalytics(infos: ConstructInfo[]) { + const trie = new Trie(); + infos.forEach(info => insertFqnInTrie(`${info.version}!${info.fqn}`, trie)); - for (const key of Object.keys(runtime.libraries).sort()) { - modules.push(`${key}=${runtime.libraries[key]}`); + const plaintextEncodedConstructs = prefixEncodeTrie(trie); + const compressedConstructs = zlib.gzipSync(Buffer.from(plaintextEncodedConstructs)).toString('base64'); + + return `v2:deflate64:${compressedConstructs}`; +} + +/** + * Splits after non-alphanumeric characters (e.g., '.', '/') in the FQN + * and insert each piece of the FQN in nested map (i.e., simple trie). + */ +function insertFqnInTrie(fqn: string, trie: Trie) { + for (const fqnPart of fqn.replace(/[^a-z0-9]/gi, '$& ').split(' ')) { + const nextLevelTreeRef = trie.get(fqnPart) ?? new Trie(); + trie.set(fqnPart, nextLevelTreeRef); + trie = nextLevelTreeRef; } - return modules.join(','); -} \ No newline at end of file + return trie; +} + +/** + * Prefix-encodes a "trie-ish" structure, using '{}' to group and ',' to separate siblings. + * + * Example input: + * ABC,ABD,AEF + * + * Example trie: + * A --> B --> C + * | \--> D + * \--> E --> F + * + * Becomes: + * A{B{C,D},EF} + */ +function prefixEncodeTrie(trie: Trie) { + let prefixEncoded = ''; + let isFirstEntryAtLevel = true; + [...trie.entries()].forEach(([key, value]) => { + if (!isFirstEntryAtLevel) { + prefixEncoded += ','; + } + isFirstEntryAtLevel = false; + prefixEncoded += key; + if (value.size > 1) { + prefixEncoded += '{'; + prefixEncoded += prefixEncodeTrie(value); + prefixEncoded += '}'; + } else { + prefixEncoded += prefixEncodeTrie(value); + } + }); + return prefixEncoded; +} diff --git a/packages/@aws-cdk/core/lib/private/resolve.ts b/packages/@aws-cdk/core/lib/private/resolve.ts index a1308f7f8f81f..77ab06ab10948 100644 --- a/packages/@aws-cdk/core/lib/private/resolve.ts +++ b/packages/@aws-cdk/core/lib/private/resolve.ts @@ -1,13 +1,42 @@ import { IConstruct } from 'constructs'; -import { DefaultTokenResolver, IPostProcessor, IResolvable, IResolveContext, ITokenResolver, StringConcat } from '../resolvable'; +import { DefaultTokenResolver, IPostProcessor, IResolvable, IResolveContext, ITokenResolver, ResolveChangeContextOptions, StringConcat } from '../resolvable'; import { TokenizedStringFragments } from '../string-fragments'; import { containsListTokenElement, TokenString, unresolved } from './encoding'; import { TokenMap } from './token-map'; // This file should not be exported to consumers, resolving should happen through Construct.resolve() - const tokenMap = TokenMap.instance(); +/** + * Resolved complex values will have a type hint applied. + * + * The type hint will be based on the type of the input value that was resolved. + * + * If the value was encoded, the type hint will be the type of the encoded value. In case + * of a plain `IResolvable`, a type hint of 'string' will be assumed. + */ +const RESOLUTION_TYPEHINT_SYM = Symbol.for('@aws-cdk/core.resolvedTypeHint'); + +/** + * Prefix used for intrinsic keys + * + * If a key with this prefix is found in an object, the actual value of the + * key doesn't matter. The value of this key will be an `[ actualKey, actualValue ]` + * tuple, and the `actualKey` will be a value which otherwise couldn't be represented + * in the types of `string | number | symbol`, which are the only possible JavaScript + * object keys. + */ +export const INTRINSIC_KEY_PREFIX = '$IntrinsicKey$'; + +/** + * Type hints for resolved values + */ +export enum ResolutionTypeHint { + STRING = 'string', + NUMBER = 'number', + LIST = 'list', +} + /** * Options to the resolve() operation * @@ -21,6 +50,36 @@ export interface IResolveOptions { preparing: boolean; resolver: ITokenResolver; prefix?: string[]; + + /** + * Whether or not to allow intrinsics in keys of an object + * + * Because keys of an object must be strings, a (resolved) intrinsic, which + * is an object, cannot be stored in that position. By default, we reject these + * intrinsics if we encounter them. + * + * If this is set to `true`, in order to store the complex value in a map, + * keys that happen to evaluate to intrinsics will be added with a unique key + * identified by an uncomming prefix, mapped to a tuple that represents the + * actual key/value-pair. The map will look like this: + * + * { + * '$IntrinsicKey$0': [ { Ref: ... }, 'value1' ], + * '$IntrinsicKey$1': [ { Ref: ... }, 'value2' ], + * 'regularKey': 'value3', + * ... + * } + * + * Callers should only set this option to `true` if they are prepared to deal with + * the object in this weird shape, and massage it back into a correct object afterwards. + * + * (A regular but uncommon string was chosen over something like symbols or + * other ways of tagging the extra values in order to simplify the implementation which + * maintains the desired behavior `resolve(resolve(x)) == resolve(x)`). + * + * @default false + */ + allowIntrinsicKeys?: boolean; } /** @@ -46,7 +105,7 @@ export function resolve(obj: any, options: IResolveOptions): any { preparing: options.preparing, scope: options.scope as IConstruct, registerPostProcessor(pp) { postProcessor = pp; }, - resolve(x: any) { return resolve(x, { ...options, prefix: newPrefix }); }, + resolve(x: any, changeOptions?: ResolveChangeContextOptions) { return resolve(x, { ...options, ...changeOptions, prefix: newPrefix }); }, }; return [context, { postProcess(x) { return postProcessor ? postProcessor.postProcess(x, context) : x; } }]; @@ -94,7 +153,7 @@ export function resolve(obj: any, options: IResolveOptions): any { const str = TokenString.forString(obj); if (str.test()) { const fragments = str.split(tokenMap.lookupToken.bind(tokenMap)); - return options.resolver.resolveString(fragments, makeContext()[0]); + return tagResolvedValue(options.resolver.resolveString(fragments, makeContext()[0]), ResolutionTypeHint.STRING); } return obj; } @@ -103,7 +162,7 @@ export function resolve(obj: any, options: IResolveOptions): any { // number - potentially decode Tokenized number // if (typeof(obj) === 'number') { - return resolveNumberToken(obj, makeContext()[0]); + return tagResolvedValue(resolveNumberToken(obj, makeContext()[0]), ResolutionTypeHint.NUMBER); } // @@ -120,7 +179,7 @@ export function resolve(obj: any, options: IResolveOptions): any { if (Array.isArray(obj)) { if (containsListTokenElement(obj)) { - return options.resolver.resolveList(obj, makeContext()[0]); + return tagResolvedValue(options.resolver.resolveList(obj, makeContext()[0]), ResolutionTypeHint.LIST); } const arr = obj @@ -136,7 +195,8 @@ export function resolve(obj: any, options: IResolveOptions): any { if (unresolved(obj)) { const [context, postProcessor] = makeContext(); - return options.resolver.resolveToken(obj, context, postProcessor); + const ret = tagResolvedValue(options.resolver.resolveToken(obj, context, postProcessor), ResolutionTypeHint.STRING); + return ret; } // @@ -151,24 +211,40 @@ export function resolve(obj: any, options: IResolveOptions): any { } const result: any = { }; + let intrinsicKeyCtr = 0; for (const key of Object.keys(obj)) { - const resolvedKey = makeContext()[0].resolve(key); - if (typeof(resolvedKey) !== 'string') { - // eslint-disable-next-line max-len - throw new Error(`"${key}" is used as the key in a map so must resolve to a string, but it resolves to: ${JSON.stringify(resolvedKey)}. Consider using "CfnJson" to delay resolution to deployment-time`); - } - - const value = makeContext(key)[0].resolve(obj[key]); + const value = makeContext(String(key))[0].resolve(obj[key]); // skip undefined if (typeof(value) === 'undefined') { continue; } - result[resolvedKey] = value; + // Simple case -- not an unresolved key + if (!unresolved(key)) { + result[key] = value; + continue; + } + + const resolvedKey = makeContext()[0].resolve(key); + if (typeof(resolvedKey) === 'string') { + result[resolvedKey] = value; + } else { + if (!options.allowIntrinsicKeys) { + // eslint-disable-next-line max-len + throw new Error(`"${String(key)}" is used as the key in a map so must resolve to a string, but it resolves to: ${JSON.stringify(resolvedKey)}. Consider using "CfnJson" to delay resolution to deployment-time`); + } + + // Can't represent this object in a JavaScript key position, but we can store it + // in value position. Use a unique symbol as the key. + result[`${INTRINSIC_KEY_PREFIX}${intrinsicKeyCtr++}`] = [resolvedKey, value]; + } } - return result; + // Because we may be called to recurse on already resolved values (that already have type hints applied) + // and we just copied those values into a fresh object, be sure to retain any type hints. + const previousTypeHint = resolvedTypeHint(obj); + return previousTypeHint ? tagResolvedValue(result, previousTypeHint) : result; } /** @@ -218,3 +294,32 @@ function resolveNumberToken(x: number, context: IResolveContext): any { if (token === undefined) { return x; } return context.resolve(token); } + +/** + * Apply a type hint to a resolved value + * + * The type hint will only be applied to objects. + * + * These type hints are used for correct JSON-ification of intrinsic values. + */ +function tagResolvedValue(value: any, typeHint: ResolutionTypeHint): any { + if (typeof value !== 'object' || value == null) { return value; } + Object.defineProperty(value, RESOLUTION_TYPEHINT_SYM, { + value: typeHint, + configurable: true, + }); + return value; +} + +/** + * Return the type hint from the given value + * + * If the value is not a resolved value (i.e, the result of resolving a token), + * `undefined` will be returned. + * + * These type hints are used for correct JSON-ification of intrinsic values. + */ +export function resolvedTypeHint(value: any): ResolutionTypeHint | undefined { + if (typeof value !== 'object' || value == null) { return undefined; } + return value[RESOLUTION_TYPEHINT_SYM]; +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/private/runtime-info.ts b/packages/@aws-cdk/core/lib/private/runtime-info.ts index b0cf266e8e11d..09962305a152f 100644 --- a/packages/@aws-cdk/core/lib/private/runtime-info.ts +++ b/packages/@aws-cdk/core/lib/private/runtime-info.ts @@ -1,95 +1,82 @@ -import { basename, dirname } from 'path'; -import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import { major as nodeMajorVersion } from './node-version'; +import { IConstruct } from 'constructs'; +import { Stack } from '../stack'; +import { Stage } from '../stage'; -// list of NPM scopes included in version reporting e.g. @aws-cdk and @aws-solutions-konstruk -const ALLOWED_SCOPES = ['@aws-cdk', '@aws-cdk-containers', '@aws-solutions-konstruk', '@aws-solutions-constructs', '@amzn']; -// list of NPM packages included in version reporting -const ALLOWED_PACKAGES = ['aws-rfdk', 'aws-cdk-lib', 'monocdk']; +const ALLOWED_FQN_PREFIXES = [ + // SCOPES + '@aws-cdk/', '@aws-cdk-containers/', '@aws-solutions-konstruk/', '@aws-solutions-constructs/', '@amzn/', + // PACKAGES + 'aws-rfdk.', 'aws-cdk-lib.', 'monocdk.', +]; /** - * Returns a list of loaded modules and their versions. + * Symbol for accessing jsii runtime information + * + * Introduced in jsii 1.19.0, cdk 1.90.0. */ -export function collectRuntimeInformation(): cxschema.RuntimeInfo { - const libraries: { [name: string]: string } = {}; - - for (const fileName of Object.keys(require.cache)) { - const pkg = findNpmPackage(fileName); - if (pkg && !pkg.private) { - libraries[pkg.name] = pkg.version; - } - } +const JSII_RUNTIME_SYMBOL = Symbol.for('jsii.rtti'); - // include only libraries that are in the allowlistLibraries list - for (const name of Object.keys(libraries)) { - let foundMatch = false; - for (const scope of ALLOWED_SCOPES) { - if (name.startsWith(`${scope}/`)) { - foundMatch = true; - } - } - foundMatch = foundMatch || ALLOWED_PACKAGES.includes(name); +/** + * Source information on a construct (class fqn and version) + */ +export interface ConstructInfo { + readonly fqn: string; + readonly version: string; +} - if (!foundMatch) { - delete libraries[name]; - } +export function constructInfoFromConstruct(construct: IConstruct): ConstructInfo | undefined { + const jsiiRuntimeInfo = Object.getPrototypeOf(construct).constructor[JSII_RUNTIME_SYMBOL]; + if (typeof jsiiRuntimeInfo === 'object' + && jsiiRuntimeInfo !== null + && typeof jsiiRuntimeInfo.fqn === 'string' + && typeof jsiiRuntimeInfo.version === 'string') { + return { fqn: jsiiRuntimeInfo.fqn, version: jsiiRuntimeInfo.version }; + } else if (jsiiRuntimeInfo) { + // There is something defined, but doesn't match our expectations. Fail fast and hard. + throw new Error(`malformed jsii runtime info for construct: '${construct.node.path}'`); } - - // add jsii runtime version - libraries['jsii-runtime'] = getJsiiAgentVersion(); - - return { libraries }; + return undefined; } /** - * Determines which NPM module a given loaded javascript file is from. - * - * The only infromation that is available locally is a list of Javascript files, - * and every source file is associated with a search path to resolve the further - * ``require`` calls made from there, which includes its own directory on disk, - * and parent directories - for example: - * - * [ '...repo/packages/aws-cdk-resources/lib/cfn/node_modules', - * '...repo/packages/aws-cdk-resources/lib/node_modules', - * '...repo/packages/aws-cdk-resources/node_modules', - * '...repo/packages/node_modules', - * // etc... - * ] - * - * We are looking for ``package.json`` that is anywhere in the tree, except it's - * in the parent directory, not in the ``node_modules`` directory. For this - * reason, we strip the ``/node_modules`` suffix off each path and use regular - * module resolution to obtain a reference to ``package.json``. - * - * @param fileName a javascript file name. - * @returns the NPM module infos (aka ``package.json`` contents), or - * ``undefined`` if the lookup was unsuccessful. + * For a given stack, walks the tree and finds the runtime info for all constructs within the tree. + * Returns the unique list of construct info present in the stack, + * as long as the construct fully-qualified names match the defined allow list. */ -function findNpmPackage(fileName: string): { name: string, version: string, private?: boolean } | undefined { - const mod = require.cache[fileName]; +export function constructInfoFromStack(stack: Stack): ConstructInfo[] { + const isDefined = (value: ConstructInfo | undefined): value is ConstructInfo => value !== undefined; - if (!mod?.paths) { - // sometimes this can be undefined. for example when querying for .json modules - // inside a jest runtime environment. - // see https://github.com/aws/aws-cdk/issues/7657 - // potentially we can remove this if it turns out to be a bug in how jest implemented the 'require' module. - return undefined; - } + const allConstructInfos = constructsInStack(stack) + .map(construct => constructInfoFromConstruct(construct)) + .filter(isDefined) + .filter(info => ALLOWED_FQN_PREFIXES.find(prefix => info.fqn.startsWith(prefix))); - // For any path in ``mod.paths`` that is a node_modules folder, use its parent directory instead. - const paths = mod?.paths.map((path: string) => basename(path) === 'node_modules' ? dirname(path) : path); + // Adds the jsii runtime as a psuedo construct for reporting purposes. + allConstructInfos.push({ + fqn: 'jsii-runtime.Runtime', + version: getJsiiAgentVersion(), + }); - try { - const packagePath = require.resolve( - // Resolution behavior changed in node 12.0.0 - https://github.com/nodejs/node/issues/27583 - nodeMajorVersion >= 12 ? './package.json' : 'package.json', - { paths }, - ); - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require(packagePath); - } catch (e) { - return undefined; - } + // Filter out duplicate values + const uniqKeys = new Set(); + return allConstructInfos.filter(construct => { + const constructKey = `${construct.fqn}@${construct.version}`; + const isDuplicate = uniqKeys.has(constructKey); + uniqKeys.add(constructKey); + return !isDuplicate; + }); +} + +/** + * Returns all constructs under the parent construct (including the parent), + * stopping when it reaches a boundary of another stack (e.g., Stack, Stage, NestedStack). + */ +function constructsInStack(construct: IConstruct): IConstruct[] { + const constructs = [construct]; + construct.node.children + .filter(child => !Stage.isStage(child) && !Stack.isStack(child)) + .forEach(child => constructs.push(...constructsInStack(child))); + return constructs; } function getJsiiAgentVersion() { diff --git a/packages/@aws-cdk/core/lib/private/token-map.ts b/packages/@aws-cdk/core/lib/private/token-map.ts index 2523037724ef0..1a5b0e1f29547 100644 --- a/packages/@aws-cdk/core/lib/private/token-map.ts +++ b/packages/@aws-cdk/core/lib/private/token-map.ts @@ -1,6 +1,6 @@ import { IResolvable } from '../resolvable'; import { TokenizedStringFragments } from '../string-fragments'; -import { Token } from '../token'; +import { isResolvableObject, Token } from '../token'; import { BEGIN_LIST_TOKEN_MARKER, BEGIN_STRING_TOKEN_MARKER, createTokenDouble, END_TOKEN_MARKER, extractTokenDouble, TokenString, VALID_KEY_CHARS, @@ -104,6 +104,7 @@ export class TokenMap { * Lookup a token from an encoded value */ public tokenFromEncoding(x: any): IResolvable | undefined { + if (isResolvableObject(x)) { return x; } if (typeof x === 'string') { return this.lookupString(x); } if (Array.isArray(x)) { return this.lookupList(x); } if (Token.isUnresolved(x)) { return x; } diff --git a/packages/@aws-cdk/core/lib/private/tree-metadata.ts b/packages/@aws-cdk/core/lib/private/tree-metadata.ts index edc13ddca435b..c6775d92a14fc 100644 --- a/packages/@aws-cdk/core/lib/private/tree-metadata.ts +++ b/packages/@aws-cdk/core/lib/private/tree-metadata.ts @@ -7,16 +7,10 @@ import { Annotations } from '../annotations'; import { Stack } from '../stack'; import { ISynthesisSession } from '../stack-synthesizers'; import { IInspectable, TreeInspector } from '../tree'; +import { ConstructInfo, constructInfoFromConstruct } from './runtime-info'; const FILE_PATH = 'tree.json'; -/** - * Symbol for accessing jsii runtime information - * - * Introduced in jsii 1.19.0, cdk 1.90.0. - */ -const JSII_RUNTIME_SYMBOL = Symbol.for('jsii.rtti'); - /** * Construct that is automatically attached to the top-level `App`. * This generates, as part of synthesis, a file containing the construct tree and the metadata for each node in the tree. @@ -49,14 +43,12 @@ export class TreeMetadata extends Construct { .filter((child) => child !== undefined) .reduce((map, child) => Object.assign(map, { [child!.id]: child }), {}); - const jsiiRuntimeInfo = Object.getPrototypeOf(construct).constructor[JSII_RUNTIME_SYMBOL]; - const node: Node = { id: construct.node.id || 'App', path: construct.node.path, children: Object.keys(childrenMap).length === 0 ? undefined : childrenMap, attributes: this.synthAttributes(construct), - constructInfo: constructInfoFromRuntimeInfo(jsiiRuntimeInfo), + constructInfo: constructInfoFromConstruct(construct), }; lookup[node.path] = node; @@ -97,16 +89,6 @@ export class TreeMetadata extends Construct { } } -function constructInfoFromRuntimeInfo(jsiiRuntimeInfo: any): ConstructInfo | undefined { - if (typeof jsiiRuntimeInfo === 'object' - && jsiiRuntimeInfo !== null - && typeof jsiiRuntimeInfo.fqn === 'string' - && typeof jsiiRuntimeInfo.version === 'string') { - return { fqn: jsiiRuntimeInfo.fqn, version: jsiiRuntimeInfo.version }; - } - return undefined; -} - interface Node { readonly id: string; readonly path: string; @@ -118,11 +100,3 @@ interface Node { */ readonly constructInfo?: ConstructInfo; } - -/** - * Source information on a construct (class fqn and version) - */ -interface ConstructInfo { - readonly fqn: string; - readonly version: string; -} diff --git a/packages/@aws-cdk/core/lib/resolvable.ts b/packages/@aws-cdk/core/lib/resolvable.ts index 495719e602f22..4a3a5a35c7df6 100644 --- a/packages/@aws-cdk/core/lib/resolvable.ts +++ b/packages/@aws-cdk/core/lib/resolvable.ts @@ -20,7 +20,7 @@ export interface IResolveContext { /** * Resolve an inner object */ - resolve(x: any): any; + resolve(x: any, options?: ResolveChangeContextOptions): any; /** * Use this postprocessor after the entire token structure has been resolved @@ -28,6 +28,18 @@ export interface IResolveContext { registerPostProcessor(postProcessor: IPostProcessor): void; } +/** + * Options that can be changed while doing a recursive resolve + */ +export interface ResolveChangeContextOptions { + /** + * Change the 'allowIntrinsicKeys' option + * + * @default - Unchanged + */ + readonly allowIntrinsicKeys?: boolean; +} + /** * Interface for values that can be resolvable later * diff --git a/packages/@aws-cdk/core/test/app.test.ts b/packages/@aws-cdk/core/test/app.test.ts index 89323365f298d..65fcec364ad16 100644 --- a/packages/@aws-cdk/core/test/app.test.ts +++ b/packages/@aws-cdk/core/test/app.test.ts @@ -5,7 +5,6 @@ import { nodeunitShim, Test } from 'nodeunit-shim'; import { CfnResource, Stack, StackProps } from '../lib'; import { Annotations } from '../lib/annotations'; import { App, AppProps } from '../lib/app'; -import { MetadataResource } from '../lib/private/metadata-resource'; function withApp(props: AppProps, block: (app: App) => void): cxapi.CloudAssembly { const app = new App({ @@ -263,90 +262,6 @@ nodeunitShim({ test.done(); }, - 'runtime library versions'(test: Test) { - v1(() => { - MetadataResource.clearModulesCache(); - - const response = withApp({ analyticsReporting: true }, app => { - const stack = new Stack(app, 'stack1'); - new CfnResource(stack, 'MyResource', { type: 'Resource::Type' }); - }); - - const stackTemplate = response.getStackByName('stack1').template; - const libs = parseModules(stackTemplate.Resources?.CDKMetadata?.Properties?.Modules); - - // eslint-disable-next-line @typescript-eslint/no-require-imports - const version = require('../package.json').version; - test.deepEqual(libs['@aws-cdk/core'], version); - test.deepEqual(libs['@aws-cdk/cx-api'], version); - test.deepEqual(libs['jsii-runtime'], `node.js/${process.version}`); - }); - test.done(); - }, - - 'CDK version'(test: Test) { - MetadataResource.clearModulesCache(); - - withCliVersion(() => { - const response = withApp({ analyticsReporting: true }, app => { - const stack = new Stack(app, 'stack1'); - new CfnResource(stack, 'MyResource', { type: 'Resource::Type' }); - }); - - const stackTemplate = response.getStackByName('stack1').template; - const libs = parseModules(stackTemplate.Resources?.CDKMetadata?.Properties?.Modules); - - // eslint-disable-next-line @typescript-eslint/no-require-imports - test.deepEqual(libs['aws-cdk'], '1.2.3'); - }); - - test.done(); - }, - - 'jsii-runtime version loaded from JSII_AGENT'(test: Test) { - process.env.JSII_AGENT = 'Java/1.2.3.4'; - MetadataResource.clearModulesCache(); - - withCliVersion(() => { - const response = withApp({ analyticsReporting: true }, app => { - const stack = new Stack(app, 'stack1'); - new CfnResource(stack, 'MyResource', { type: 'Resource::Type' }); - }); - - const stackTemplate = response.getStackByName('stack1').template; - const libs = parseModules(stackTemplate.Resources?.CDKMetadata?.Properties?.Modules); - - test.deepEqual(libs['jsii-runtime'], 'Java/1.2.3.4'); - }); - - delete process.env.JSII_AGENT; - test.done(); - }, - - 'version reporting includes only @aws-cdk, aws-cdk and jsii libraries'(test: Test) { - v1(() => { - MetadataResource.clearModulesCache(); - - const response = withApp({ analyticsReporting: true }, app => { - const stack = new Stack(app, 'stack1'); - new CfnResource(stack, 'MyResource', { type: 'Resource::Type' }); - }); - - const stackTemplate = response.getStackByName('stack1').template; - const libs = parseModules(stackTemplate.Resources?.CDKMetadata?.Properties?.Modules); - const libNames = Object.keys(libs).sort(); - - test.deepEqual(libNames, [ - '@aws-cdk/cloud-assembly-schema', - '@aws-cdk/core', - '@aws-cdk/cx-api', - '@aws-cdk/region-info', - 'jsii-runtime', - ]); - }); - test.done(); - }, - 'deep stack is shown and synthesized properly'(test: Test) { // WHEN const response = withApp({}, (app) => { @@ -423,42 +338,3 @@ class MyConstruct extends Construct { new CfnResource(this, 'r2', { type: 'ResourceType2', properties: { FromContext: this.node.tryGetContext('ctx1') } }); } } - -function parseModules(x?: string): Record { - if (x === undefined) { return {}; } - - const ret: Record = {}; - for (const clause of x.split(',')) { - const [key, value] = clause.split('='); - if (key !== undefined && value !== undefined) { - ret[key] = value; - } - } - return ret; -} - -/** - * Set the CLI_VERSION_ENV environment variable - * - * This is necessary to get the Stack to emit the metadata resource - */ -function withCliVersion(block: () => A): A { - process.env[cxapi.CLI_VERSION_ENV] = '1.2.3'; - try { - return block(); - } finally { - delete process.env[cxapi.CLI_VERSION_ENV]; - } -} - -function v1(block: () => void) { - onVersion(1, block); -} - -function onVersion(version: number, block: () => void) { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const mv: number = require('../../../../release.json').majorVersion; - if (version === mv) { - block(); - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/core/test/cloudformation-json.test.ts b/packages/@aws-cdk/core/test/cloudformation-json.test.ts index e9d850eb178a2..e9a0957a86269 100644 --- a/packages/@aws-cdk/core/test/cloudformation-json.test.ts +++ b/packages/@aws-cdk/core/test/cloudformation-json.test.ts @@ -1,12 +1,36 @@ -import { nodeunitShim, Test } from 'nodeunit-shim'; -import { App, CfnOutput, Fn, Lazy, Stack, Token } from '../lib'; +import { App, Aws, CfnOutput, Fn, IPostProcessor, IResolvable, IResolveContext, Lazy, Stack, Token } from '../lib'; import { Intrinsic } from '../lib/private/intrinsic'; import { evaluateCFN } from './evaluate-cfn'; -nodeunitShim({ - 'string tokens can be JSONified and JSONification can be reversed'(test: Test) { - const stack = new Stack(); +let app: App; +let stack: Stack; +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack'); +}); + +test('JSONification of literals looks like JSON.stringify', () => { + const structure = { + undefinedProp: undefined, + nestedObject: { + prop1: undefined, + prop2: 'abc', + prop3: 42, + prop4: [1, 2, 3], + }, + }; + + expect(stack.resolve(stack.toJsonString(structure))).toEqual(JSON.stringify(structure)); + expect(stack.resolve(stack.toJsonString(structure, 2))).toEqual(JSON.stringify(structure, undefined, 2)); +}); + +test('JSONification of undefined leads to undefined', () => { + expect(stack.resolve(stack.toJsonString(undefined))).toEqual(undefined); +}); + +describe('tokens that return literals', () => { + test('string tokens can be JSONified and JSONification can be reversed', () => { for (const token of tokensThatResolveTo('woof woof')) { // GIVEN const fido = { name: 'Fido', speaks: token }; @@ -15,15 +39,11 @@ nodeunitShim({ const resolved = stack.resolve(stack.toJsonString(fido)); // THEN - test.deepEqual(evaluateCFN(resolved), '{"name":"Fido","speaks":"woof woof"}'); + expect(evaluateCFN(resolved)).toEqual('{"name":"Fido","speaks":"woof woof"}'); } + }); - test.done(); - }, - - 'string tokens can be embedded while being JSONified'(test: Test) { - const stack = new Stack(); - + test('string tokens can be embedded while being JSONified', () => { for (const token of tokensThatResolveTo('woof woof')) { // GIVEN const fido = { name: 'Fido', speaks: `deep ${token}` }; @@ -32,57 +52,108 @@ nodeunitShim({ const resolved = stack.resolve(stack.toJsonString(fido)); // THEN - test.deepEqual(evaluateCFN(resolved), '{"name":"Fido","speaks":"deep woof woof"}'); + expect(evaluateCFN(resolved)).toEqual('{"name":"Fido","speaks":"deep woof woof"}'); } + }); - test.done(); - }, - - 'constant string has correct amount of quotes applied'(test: Test) { - const stack = new Stack(); - + test('constant string has correct amount of quotes applied', () => { const inputString = 'Hello, "world"'; // WHEN const resolved = stack.resolve(stack.toJsonString(inputString)); // THEN - test.deepEqual(evaluateCFN(resolved), JSON.stringify(inputString)); - - test.done(); - }, + expect(evaluateCFN(resolved)).toEqual(JSON.stringify(inputString)); + }); - 'integer Tokens behave correctly in stringification and JSONification'(test: Test) { + test('integer Tokens behave correctly in stringification and JSONification', () => { // GIVEN - const stack = new Stack(); const num = new Intrinsic(1); const embedded = `the number is ${num}`; // WHEN - test.equal(evaluateCFN(stack.resolve(embedded)), 'the number is 1'); - test.equal(evaluateCFN(stack.resolve(stack.toJsonString({ embedded }))), '{"embedded":"the number is 1"}'); - test.equal(evaluateCFN(stack.resolve(stack.toJsonString({ num }))), '{"num":1}'); + expect(evaluateCFN(stack.resolve(embedded))).toEqual('the number is 1'); + expect(evaluateCFN(stack.resolve(stack.toJsonString({ embedded })))).toEqual('{"embedded":"the number is 1"}'); + expect(evaluateCFN(stack.resolve(stack.toJsonString({ num })))).toEqual('{"num":1}'); + }); + + test('String-encoded lazies do not have quotes applied if they return objects', () => { + // This is unfortunately crazy behavior, but we have some clients already taking a + // dependency on the fact that `Lazy.stringValue({ produce: () => [...some list...] })` + // does not apply quotes but just renders the list. + + // GIVEN + const someList = Lazy.stringValue({ produce: () => [1, 2, 3] as any }); + + // WHEN + expect(evaluateCFN(stack.resolve(stack.toJsonString({ someList })))).toEqual('{"someList":[1,2,3]}'); + }); + + test('Literal-resolving List Tokens do not have quotes applied', () => { + // GIVEN + const someList = Token.asList([1, 2, 3]); + + // WHEN + expect(evaluateCFN(stack.resolve(stack.toJsonString({ someList })))).toEqual('{"someList":[1,2,3]}'); + }); + + test('Intrinsic-resolving List Tokens do not have quotes applied', () => { + // GIVEN + const someList = Token.asList(new Intrinsic({ Ref: 'Thing' })); + + // WHEN + expect(stack.resolve(stack.toJsonString({ someList }))).toEqual({ + 'Fn::Join': ['', [ + '{"someList":', + { 'Fn::GetAtt': [expect.stringContaining('CdkJsonStringify'), 'Value'] }, + '}', + ]], + }); + }); - test.done(); - }, - 'tokens in strings survive additional TokenJSON.stringification()'(test: Test) { + test('tokens in strings survive additional TokenJSON.stringification()', () => { // GIVEN - const stack = new Stack(); for (const token of tokensThatResolveTo('pong!')) { // WHEN const stringified = stack.toJsonString(`ping? ${token}`); // THEN - test.equal(evaluateCFN(stack.resolve(stringified)), '"ping? pong!"'); + expect(evaluateCFN(stack.resolve(stringified))).toEqual('"ping? pong!"'); } + }); + + test('Doubly nested strings evaluate correctly in JSON context', () => { + // WHEN + const fidoSays = Lazy.stringValue({ produce: () => 'woof' }); + + // WHEN + const resolved = stack.resolve(stack.toJsonString({ + information: `Did you know that Fido says: ${fidoSays}`, + })); + + // THEN + expect(evaluateCFN(resolved)).toEqual('{"information":"Did you know that Fido says: woof"}'); + }); + + test('Quoted strings in embedded JSON context are escaped', () => { + // GIVEN + const fidoSays = Lazy.stringValue({ produce: () => '"woof"' }); - test.done(); - }, + // WHEN + const resolved = stack.resolve(stack.toJsonString({ + information: `Did you know that Fido says: ${fidoSays}`, + })); + + // THEN + expect(evaluateCFN(resolved)).toEqual('{"information":"Did you know that Fido says: \\"woof\\""}'); + }); - 'intrinsic Tokens embed correctly in JSONification'(test: Test) { +}); + +describe('tokens returning CloudFormation intrinsics', () => { + test('intrinsic Tokens embed correctly in JSONification', () => { // GIVEN - const stack = new Stack(); const bucketName = new Intrinsic({ Ref: 'MyBucket' }); // WHEN @@ -90,13 +161,10 @@ nodeunitShim({ // THEN const context = { MyBucket: 'TheName' }; - test.equal(evaluateCFN(resolved, context), '{"theBucket":"TheName"}'); - - test.done(); - }, + expect(evaluateCFN(resolved, context)).toEqual('{"theBucket":"TheName"}'); + }); - 'fake intrinsics are serialized to objects'(test: Test) { - const stack = new Stack(); + test('fake intrinsics are serialized to objects', () => { const fakeIntrinsics = new Intrinsic({ a: { 'Fn::GetArtifactAtt': { @@ -112,16 +180,13 @@ nodeunitShim({ }); const stringified = stack.toJsonString(fakeIntrinsics); - test.equal(evaluateCFN(stack.resolve(stringified)), + expect(evaluateCFN(stack.resolve(stringified))).toEqual( '{"a":{"Fn::GetArtifactAtt":{"key":"val"}},"b":{"Fn::GetParam":["val1","val2"]}}'); + }); - test.done(); - }, - - 'embedded string literals in intrinsics are escaped when calling TokenJSON.stringify()'(test: Test) { + test('embedded string literals in intrinsics are escaped when calling TokenJSON.stringify()', () => { // GIVEN - const stack = new Stack(); - const token = Fn.join('', ['Hello', 'This\nIs', 'Very "cool"']); + const token = Fn.join('', ['Hello ', Token.asString({ Ref: 'Planet' }), ', this\nIs', 'Very "cool"']); // WHEN const resolved = stack.resolve(stack.toJsonString({ @@ -130,15 +195,42 @@ nodeunitShim({ })); // THEN - const expected = '{"literal":"I can also \\"contain\\" quotes","token":"HelloThis\\nIsVery \\"cool\\""}'; - test.equal(evaluateCFN(resolved), expected); + const context = { Planet: 'World' }; + const expected = '{"literal":"I can also \\"contain\\" quotes","token":"Hello World, this\\nIsVery \\"cool\\""}'; + expect(evaluateCFN(resolved, context)).toEqual(expected); + }); - test.done(); - }, + test('embedded string literals are escaped in Fn.sub (implicit references)', () => { + // GIVEN + const token = Fn.sub('I am in account "${AWS::AccountId}"'); - 'Tokens in Tokens are handled correctly'(test: Test) { + // WHEN + const resolved = stack.resolve(stack.toJsonString({ token })); + + // THEN + const context = { 'AWS::AccountId': '1234' }; + const expected = '{"token":"I am in account \\"1234\\""}'; + expect(evaluateCFN(resolved, context)).toEqual(expected); + }); + + test('embedded string literals are escaped in Fn.sub (explicit references)', () => { + // GIVEN + const token = Fn.sub('I am in account "${Acct}", also wanted to say: ${Also}', { + Acct: Aws.ACCOUNT_ID, + Also: '"hello world"', + }); + + // WHEN + const resolved = stack.resolve(stack.toJsonString({ token })); + + // THEN + const context = { 'AWS::AccountId': '1234' }; + const expected = '{"token":"I am in account \\"1234\\", also wanted to say: \\"hello world\\""}'; + expect(evaluateCFN(resolved, context)).toEqual(expected); + }); + + test('Tokens in Tokens are handled correctly', () => { // GIVEN - const stack = new Stack(); const bucketName = new Intrinsic({ Ref: 'MyBucket' }); const combinedName = Fn.join('', ['The bucket name is ', bucketName.toString()]); @@ -147,14 +239,25 @@ nodeunitShim({ // THEN const context = { MyBucket: 'TheName' }; - test.equal(evaluateCFN(resolved, context), '{"theBucket":"The bucket name is TheName"}'); + expect(evaluateCFN(resolved, context)).toEqual('{"theBucket":"The bucket name is TheName"}'); + }); + + test('Intrinsics in postprocessors are handled correctly', () => { + // GIVEN + const bucketName = new Intrinsic({ Ref: 'MyBucket' }); + const combinedName = new DummyPostProcessor(['this', 'is', bucketName]); + + // WHEN + const resolved = stack.resolve(stack.toJsonString({ theBucket: combinedName })); - test.done(); - }, + // THEN + expect(resolved).toEqual({ + 'Fn::Join': ['', ['{"theBucket":["this","is","', { Ref: 'MyBucket' }, '"]}']], + }); + }); - 'Doubly nested strings evaluate correctly in JSON context'(test: Test) { + test('Doubly nested strings evaluate correctly in JSON context', () => { // WHEN - const stack = new Stack(); const fidoSays = Lazy.string({ produce: () => 'woof' }); // WHEN @@ -163,14 +266,11 @@ nodeunitShim({ })); // THEN - test.deepEqual(evaluateCFN(resolved), '{"information":"Did you know that Fido says: woof"}'); + expect(evaluateCFN(resolved)).toEqual('{"information":"Did you know that Fido says: woof"}'); + }); - test.done(); - }, - - 'Doubly nested intrinsics evaluate correctly in JSON context'(test: Test) { + test('Doubly nested intrinsics evaluate correctly in JSON context', () => { // GIVEN - const stack = new Stack(); const fidoSays = Lazy.any({ produce: () => ({ Ref: 'Something' }) }); // WHEN @@ -180,14 +280,10 @@ nodeunitShim({ // THEN const context = { Something: 'woof woof' }; - test.deepEqual(evaluateCFN(resolved, context), '{"information":"Did you know that Fido says: woof woof"}'); - - test.done(); - }, + expect(evaluateCFN(resolved, context)).toEqual('{"information":"Did you know that Fido says: woof woof"}'); + }); - 'Quoted strings in embedded JSON context are escaped'(test: Test) { - // GIVEN - const stack = new Stack(); + test('Nested strings are quoted correctly', () => { const fidoSays = Lazy.string({ produce: () => '"woof"' }); // WHEN @@ -196,14 +292,11 @@ nodeunitShim({ })); // THEN - test.deepEqual(evaluateCFN(resolved), '{"information":"Did you know that Fido says: \\"woof\\""}'); + expect(evaluateCFN(resolved)).toEqual('{"information":"Did you know that Fido says: \\"woof\\""}'); + }); - test.done(); - }, - - 'cross-stack references are also properly converted by toJsonString()'(test: Test) { + test('cross-stack references are also properly converted by toJsonString()', () => { // GIVEN - const app = new App(); const stack1 = new Stack(app, 'Stack1'); const stack2 = new Stack(app, 'Stack2'); @@ -217,7 +310,7 @@ nodeunitShim({ // THEN const asm = app.synth(); - test.deepEqual(asm.getStackByName('Stack2').template, { + expect(asm.getStackByName('Stack2').template).toEqual({ Outputs: { Stack1Id: { Value: { @@ -232,11 +325,40 @@ nodeunitShim({ }, }, }); + }); - test.done(); - }, + test('Intrinsics can occur in key position', () => { + // GIVEN + const bucketName = Token.asString({ Ref: 'MyBucket' }); - 'Every Token used inside a JSONified string is given an opportunity to be uncached'(test: Test) { + // WHEN + const resolved = stack.resolve(stack.toJsonString({ + [bucketName]: 'Is Cool', + [`${bucketName} Is`]: 'Cool', + })); + + // THEN + const context = { MyBucket: 'Harry' }; + expect(evaluateCFN(resolved, context)).toEqual('{"Harry":"Is Cool","Harry Is":"Cool"}'); + }); + + test('toJsonString() can be used recursively', () => { + // GIVEN + const bucketName = Token.asString({ Ref: 'MyBucket' }); + + // WHEN + const embeddedJson = stack.toJsonString({ message: `the bucket name is ${bucketName}` }); + const outerJson = stack.toJsonString({ embeddedJson }); + + // THEN + const evaluatedJson = evaluateCFN(stack.resolve(outerJson), { + MyBucket: 'Bucky', + }); + expect(evaluatedJson).toEqual('{"embeddedJson":"{\\"message\\":\\"the bucket name is Bucky\\"}"}'); + expect(JSON.parse(JSON.parse(evaluatedJson).embeddedJson).message).toEqual('the bucket name is Bucky'); + }); + + test('Every Token used inside a JSONified string is given an opportunity to be uncached', () => { // Check that tokens aren't accidentally fully resolved by the first invocation/resolution // of toJsonString(). On every evaluation, Tokens referenced inside the structure should be // given a chance to be either cached or uncached. @@ -244,10 +366,6 @@ nodeunitShim({ // (NOTE: This does not check whether the implementation of toJsonString() itself is cached or // not; that depends on aws/aws-cdk#11224 and should be done in a different PR). - // GIVEN - const app = new App(); - const stack = new Stack(app, 'Stack1'); - // WHEN let counter = 0; const counterString = Token.asString({ resolve: () => `${++counter}` }); @@ -256,11 +374,29 @@ nodeunitShim({ // THEN expect(stack.resolve(jsonString)).toEqual('{"counterString":"1"}'); expect(stack.resolve(jsonString)).toEqual('{"counterString":"2"}'); + }); +}); + +test('JSON strings nested inside JSON strings have correct quoting', () => { + // GIVEN + const payload = stack.toJsonString({ + message: Fn.sub('I am in account "${AWS::AccountId}"'), + }); + + // WHEN + const resolved = stack.resolve(stack.toJsonString({ payload })); + + // THEN + const context = { 'AWS::AccountId': '1234' }; + const expected = '{"payload":"{\\"message\\":\\"I am in account \\\\\\"1234\\\\\\"\\"}"}'; + const evaluated = evaluateCFN(resolved, context); + expect(evaluated).toEqual(expected); - test.done(); - }, + // Is this even correct? Let's ask JavaScript because I have trouble reading this many backslashes. + expect(JSON.parse(JSON.parse(evaluated).payload).message).toEqual('I am in account "1234"'); }); + /** * Return two Tokens, one of which evaluates to a Token directly, one which evaluates to it lazily */ @@ -270,3 +406,20 @@ function tokensThatResolveTo(value: any): Token[] { Lazy.any({ produce: () => value }), ]; } + +class DummyPostProcessor implements IResolvable, IPostProcessor { + public readonly creationStack: string[]; + + constructor(private readonly value: any) { + this.creationStack = ['test']; + } + + public resolve(context: IResolveContext) { + context.registerPostProcessor(this); + return context.resolve(this.value); + } + + public postProcess(o: any, _context: IResolveContext): any { + return o; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/test/evaluate-cfn.ts b/packages/@aws-cdk/core/test/evaluate-cfn.ts index 6d60949cc3193..af07209c7e5a7 100644 --- a/packages/@aws-cdk/core/test/evaluate-cfn.ts +++ b/packages/@aws-cdk/core/test/evaluate-cfn.ts @@ -42,16 +42,8 @@ export function evaluateCFN(object: any, context: {[key: string]: string} = {}): return context[key]; }, - 'Fn::Sub'(argument: string | [string, Record]) { - let template; - let placeholders: Record; - if (Array.isArray(argument)) { - template = argument[0]; - placeholders = evaluate(argument[1]); - } else { - template = argument; - placeholders = context; - } + 'Fn::Sub'(template: string, explicitPlaceholders?: Record) { + const placeholders = explicitPlaceholders ? evaluate(explicitPlaceholders) : context; if (typeof template !== 'string') { throw new Error('The first argument to {Fn::Sub} must be a string literal (cannot be the result of an expression)'); @@ -79,7 +71,7 @@ export function evaluateCFN(object: any, context: {[key: string]: string} = {}): const ret: {[key: string]: any} = {}; for (const key of Object.keys(obj)) { - ret[key] = evaluateCFN(obj[key]); + ret[key] = evaluate(obj[key]); } return ret; } diff --git a/packages/@aws-cdk/core/test/metadata-resource.test.ts b/packages/@aws-cdk/core/test/metadata-resource.test.ts new file mode 100644 index 0000000000000..1fc7d1e80fe19 --- /dev/null +++ b/packages/@aws-cdk/core/test/metadata-resource.test.ts @@ -0,0 +1,149 @@ +import * as zlib from 'zlib'; +import { Construct } from 'constructs'; +import { App, Stack } from '../lib'; +import { formatAnalytics } from '../lib/private/metadata-resource'; + +import { ConstructInfo } from '../lib/private/runtime-info'; + +describe('MetadataResource', () => { + let app: App; + let stack: Stack; + + beforeEach(() => { + app = new App({ + analyticsReporting: true, + }); + stack = new Stack(app, 'Stack'); + }); + + test('is not included if the region is known and metadata is not available', () => { + new Stack(app, 'StackUnavailable', { + env: { region: 'definitely-no-metadata-resource-available-here' }, + }); + + const stackTemplate = app.synth().getStackByName('StackUnavailable').template; + + expect(stackTemplate.Resources?.CDKMetadata).toBeUndefined(); + }); + + test('is included if the region is known and metadata is available', () => { + new Stack(app, 'StackPresent', { + env: { region: 'us-east-1' }, + }); + + const stackTemplate = app.synth().getStackByName('StackPresent').template; + + expect(stackTemplate.Resources?.CDKMetadata).toBeDefined(); + }); + + test('is included if the region is unknown with conditions', () => { + new Stack(app, 'StackUnknown'); + + const stackTemplate = app.synth().getStackByName('StackUnknown').template; + + expect(stackTemplate.Resources?.CDKMetadata).toBeDefined(); + expect(stackTemplate.Resources?.CDKMetadata?.Condition).toBeDefined(); + }); + + test('includes the formatted Analytics property', () => { + // A very simple check that the jsii runtime psuedo-construct is present. + // This check works whether we're running locally or on CodeBuild, on v1 or v2. + // Other tests(in app.test.ts) will test version-specific results. + expect(stackAnalytics()).toMatch(/jsii-runtime.Runtime/); + }); + + test('includes the current jsii runtime version', () => { + process.env.JSII_AGENT = 'Java/1.2.3.4'; + + expect(stackAnalytics()).toContain('Java/1.2.3.4!jsii-runtime.Runtime'); + delete process.env.JSII_AGENT; + }); + + test('includes constructs added to the stack', () => { + new TestConstruct(stack, 'Test'); + + expect(stackAnalytics()).toContain('FakeVersion.2.3!@amzn/core.TestConstruct'); + }); + + test('only includes constructs in the allow list', () => { + new TestThirdPartyConstruct(stack, 'Test'); + + expect(stackAnalytics()).not.toContain('TestConstruct'); + }); + + function stackAnalytics(stackName: string = 'Stack') { + const encodedAnalytics = app.synth().getStackByName(stackName).template.Resources?.CDKMetadata?.Properties?.Analytics as string; + return plaintextConstructsFromAnalytics(encodedAnalytics); + } +}); + +describe('formatAnalytics', () => { + test('analytics are formatted with a prefix of v2:deflate64:', () => { + const constructInfo = [{ fqn: 'aws-cdk-lib.Construct', version: '1.2.3' }]; + + expect(formatAnalytics(constructInfo)).toMatch(/v2:deflate64:.*/); + }); + + test('single construct', () => { + const constructInfo = [{ fqn: 'aws-cdk-lib.Construct', version: '1.2.3' }]; + + expectAnalytics(constructInfo, '1.2.3!aws-cdk-lib.Construct'); + }); + + test('common prefixes with same versions are combined', () => { + const constructInfo = [ + { fqn: 'aws-cdk-lib.Construct', version: '1.2.3' }, + { fqn: 'aws-cdk-lib.CfnResource', version: '1.2.3' }, + { fqn: 'aws-cdk-lib.Stack', version: '1.2.3' }, + ]; + + expectAnalytics(constructInfo, '1.2.3!aws-cdk-lib.{Construct,CfnResource,Stack}'); + }); + + test('nested modules with common prefixes and same versions are combined', () => { + const constructInfo = [ + { fqn: 'aws-cdk-lib.Construct', version: '1.2.3' }, + { fqn: 'aws-cdk-lib.CfnResource', version: '1.2.3' }, + { fqn: 'aws-cdk-lib.Stack', version: '1.2.3' }, + { fqn: 'aws-cdk-lib.aws_servicefoo.CoolResource', version: '1.2.3' }, + { fqn: 'aws-cdk-lib.aws_servicefoo.OtherResource', version: '1.2.3' }, + ]; + + expectAnalytics(constructInfo, '1.2.3!aws-cdk-lib.{Construct,CfnResource,Stack,aws_servicefoo.{CoolResource,OtherResource}}'); + }); + + test('constructs are grouped by version', () => { + const constructInfo = [ + { fqn: 'aws-cdk-lib.Construct', version: '1.2.3' }, + { fqn: 'aws-cdk-lib.CfnResource', version: '1.2.3' }, + { fqn: 'aws-cdk-lib.Stack', version: '1.2.3' }, + { fqn: 'aws-cdk-lib.CoolResource', version: '0.1.2' }, + { fqn: 'aws-cdk-lib.OtherResource', version: '0.1.2' }, + ]; + + expectAnalytics(constructInfo, '1.2.3!aws-cdk-lib.{Construct,CfnResource,Stack},0.1.2!aws-cdk-lib.{CoolResource,OtherResource}'); + }); + + // Compares the output of formatAnalytics with an expected (plaintext) output. + // For ease of testing, the plaintext versions are compared rather than the encoded versions. + function expectAnalytics(constructs: ConstructInfo[], expectedPlaintext: string) { + expect(plaintextConstructsFromAnalytics(formatAnalytics(constructs))).toEqual(expectedPlaintext); + } + +}); + +function plaintextConstructsFromAnalytics(analytics: string) { + return zlib.gunzipSync(Buffer.from(analytics.split(':')[2], 'base64')).toString('utf-8'); +} + +const JSII_RUNTIME_SYMBOL = Symbol.for('jsii.rtti'); + +class TestConstruct extends Construct { + // @ts-ignore + private static readonly [JSII_RUNTIME_SYMBOL] = { fqn: '@amzn/core.TestConstruct', version: 'FakeVersion.2.3' } +} + +class TestThirdPartyConstruct extends Construct { + // @ts-ignore + private static readonly [JSII_RUNTIME_SYMBOL] = { fqn: 'mycoolthing.TestConstruct', version: '1.2.3' } +} diff --git a/packages/@aws-cdk/core/test/runtime-info.test.ts b/packages/@aws-cdk/core/test/runtime-info.test.ts index 67f931bb63ec5..43f542e6e84cd 100644 --- a/packages/@aws-cdk/core/test/runtime-info.test.ts +++ b/packages/@aws-cdk/core/test/runtime-info.test.ts @@ -1,73 +1,170 @@ -import * as fs from 'fs'; -import * as os from 'os'; import * as path from 'path'; -import { nodeunitShim, Test } from 'nodeunit-shim'; -import { collectRuntimeInformation } from '../lib/private/runtime-info'; +import { Construct } from 'constructs'; +import { App, NestedStack, Stack, Stage } from '../lib'; +import { constructInfoFromConstruct, constructInfoFromStack } from '../lib/private/runtime-info'; + +const JSII_RUNTIME_SYMBOL = Symbol.for('jsii.rtti'); + +let app: App; +let stack: Stack; +let _cdkVersion: string | undefined = undefined; + +// The runtime metadata this test relies on is only available if the most +// recent compile has happened using 'jsii', as the jsii compiler injects +// this metadata. +// +// If the most recent compile was using 'tsc', the metadata will not have +// been injected, and the test suite will fail. +// +// Tolerate `tsc` builds locally, but not on CodeBuild. +const codeBuild = !!process.env.CODEBUILD_BUILD_ID; +const moduleCompiledWithTsc = constructInfoFromConstruct(new Stack())?.fqn === 'constructs.Construct'; +let describeTscSafe = describe; +if (moduleCompiledWithTsc && !codeBuild) { + // eslint-disable-next-line + console.error('It appears this module was compiled with `tsc` instead of `jsii` in a local build. Skipping this test suite.'); + describeTscSafe = describe.skip; +} + +beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + analyticsReporting: true, + }); +}); -nodeunitShim({ - 'version reporting includes @aws-solutions-konstruk libraries'(test: Test) { - const pkgdir = fs.mkdtempSync(path.join(os.tmpdir(), 'runtime-info-konstruk-fixture')); - const mockVersion = '1.2.3'; +describeTscSafe('constructInfoFromConstruct', () => { + test('returns fqn and version for core constructs', () => { + const constructInfo = constructInfoFromConstruct(stack); + expect(constructInfo).toBeDefined(); + expect(constructInfo?.fqn).toEqual('@aws-cdk/core.Stack'); + expect(constructInfo?.version).toEqual(localCdkVersion()); + }); + + test('returns jsii runtime info if present', () => { + const construct = new TestConstruct(stack, 'TestConstruct'); + + const constructInfo = constructInfoFromConstruct(construct); + expect(constructInfo?.fqn).toEqual('@aws-cdk/test.TestConstruct'); + }); + + test('throws if the jsii runtime info is not as expected', () => { + const constructRuntimeInfoNotObject = new class extends Construct { + // @ts-ignore + private static readonly [JSII_RUNTIME_SYMBOL] = 'HelloWorld'; + }(stack, 'RuntimeNotObject'); + const constructWithWrongRuntimeInfoMembers = new class extends Construct { + // @ts-ignore + private static readonly [JSII_RUNTIME_SYMBOL] = { foo: 'bar' }; + }(stack, 'RuntimeWrongMembers'); + const constructWithWrongRuntimeInfoTypes = new class extends Construct { + // @ts-ignore + private static readonly [JSII_RUNTIME_SYMBOL] = { fqn: 42, version: { name: '0.0.0' } }; + }(stack, 'RuntimeWrongTypes'); + + const errorMessage = 'malformed jsii runtime info for construct'; + [constructRuntimeInfoNotObject, constructWithWrongRuntimeInfoMembers, constructWithWrongRuntimeInfoTypes].forEach(construct => { + expect(() => constructInfoFromConstruct(construct)).toThrow(errorMessage); + }); + }); +}); - fs.writeFileSync(path.join(pkgdir, 'index.js'), 'module.exports = \'this is foo\';'); - fs.writeFileSync(path.join(pkgdir, 'package.json'), JSON.stringify({ - name: '@aws-solutions-konstruk/foo', - version: mockVersion, - })); +describeTscSafe('constructInfoForStack', () => { + test('returns stack itself and jsii runtime if stack is empty', () => { + const constructInfos = constructInfoFromStack(stack); - // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies - require(pkgdir); + expect(constructInfos.length).toEqual(2); - const runtimeInfo = collectRuntimeInformation(); + const stackInfo = constructInfos.find(i => /Stack/.test(i.fqn)); + const jsiiInfo = constructInfos.find(i => i.fqn === 'jsii-runtime.Runtime'); + expect(stackInfo?.fqn).toEqual('@aws-cdk/core.Stack'); + expect(stackInfo?.version).toEqual(localCdkVersion()); + expect(jsiiInfo?.version).toMatch(/node.js/); + }); - // eslint-disable-next-line @typescript-eslint/no-require-imports - test.deepEqual(runtimeInfo.libraries['@aws-solutions-konstruk/foo'], mockVersion); - test.done(); - }, + test('returns info for constructs added to the stack', () => { + new TestConstruct(stack, 'TestConstruct'); - 'version reporting finds aws-rfdk package'(test: Test) { - const pkgdir = fs.mkdtempSync(path.join(os.tmpdir(), 'runtime-info-rfdk')); - const mockVersion = '1.2.3'; + const constructInfos = constructInfoFromStack(stack); - fs.writeFileSync(path.join(pkgdir, 'index.js'), 'module.exports = \'this is foo\';'); - fs.writeFileSync(path.join(pkgdir, 'package.json'), JSON.stringify({ - name: 'aws-rfdk', - version: mockVersion, - })); + expect(constructInfos.length).toEqual(3); + expect(constructInfos.map(info => info.fqn)).toContain('@aws-cdk/test.TestConstruct'); + }); - // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies - require(pkgdir); + test('returns unique info (no duplicates)', () => { + new TestConstruct(stack, 'TestConstruct1'); + new TestConstruct(stack, 'TestConstruct2'); - const runtimeInfo = collectRuntimeInformation(); + const constructInfos = constructInfoFromStack(stack); - // eslint-disable-next-line @typescript-eslint/no-require-imports - test.deepEqual(runtimeInfo.libraries['aws-rfdk'], mockVersion); - test.done(); - }, - - 'version reporting finds no version with no associated package.json'(test: Test) { - const pkgdir = fs.mkdtempSync(path.join(os.tmpdir(), 'runtime-info-find-npm-package-fixture')); - const mockVersion = '1.2.3'; - - fs.writeFileSync(path.join(pkgdir, 'index.js'), 'module.exports = \'this is bar\';'); - fs.mkdirSync(path.join(pkgdir, 'bar')); - fs.writeFileSync(path.join(pkgdir, 'bar', 'package.json'), JSON.stringify({ - name: '@aws-solutions-konstruk/bar', - version: mockVersion, - })); - - // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies - require(pkgdir); - - const cwd = process.cwd(); - - // Switch to `bar` where the package.json is, then resolve version. Fails when module.resolve - // is passed an empty string in the paths array. - process.chdir(path.join(pkgdir, 'bar')); - const runtimeInfo = collectRuntimeInformation(); - process.chdir(cwd); - - test.equal(runtimeInfo.libraries['@aws-solutions-konstruk/bar'], undefined); - test.done(); - }, + expect(constructInfos.length).toEqual(3); + expect(constructInfos.map(info => info.fqn)).toContain('@aws-cdk/test.TestConstruct'); + }); + + test('returns info from nested constructs', () => { + new class extends Construct { + constructor(scope: Construct, id: string) { + super(scope, id); + new TestConstruct(this, 'TestConstruct'); + } + }(stack, 'Nested'); + + const constructInfos = constructInfoFromStack(stack); + + expect(constructInfos.map(info => info.fqn)).toContain('@aws-cdk/test.TestConstruct'); + }); + + test('does not return info from nested stacks', () => { + new class extends Construct { + constructor(scope: Construct, id: string) { + super(scope, id); + + new TestConstruct(this, 'TestConstruct'); + + new class extends Stack { + // @ts-ignore + private static readonly [JSII_RUNTIME_SYMBOL] = { fqn: '@aws-cdk/test.TestStackInsideStack', version: localCdkVersion() } + }(this, 'StackInsideStack'); + + new class extends NestedStack { + // @ts-ignore + private static readonly [JSII_RUNTIME_SYMBOL] = { fqn: '@aws-cdk/test.TestNestedStackInsideStack', version: localCdkVersion() } + }(this, 'NestedStackInsideStack'); + + new class extends Stage { + // @ts-ignore + private static readonly [JSII_RUNTIME_SYMBOL] = { fqn: '@aws-cdk/test.TestStageInsideStack', version: localCdkVersion() } + }(this, 'StageInsideStack'); + } + }(stack, 'ParentConstruct'); + + const constructInfos = constructInfoFromStack(stack); + + const fqns = constructInfos.map(info => info.fqn); + expect(fqns).toContain('@aws-cdk/test.TestConstruct'); + expect(fqns).not.toContain('@aws-cdk/test.TestStackInsideStack'); + expect(fqns).not.toContain('@aws-cdk/test.TestNestedStackInsideStack'); + expect(fqns).not.toContain('@aws-cdk/test.TestStageInsideStack'); + }); }); + +class TestConstruct extends Construct { + // @ts-ignore + private static readonly [JSII_RUNTIME_SYMBOL] = { fqn: '@aws-cdk/test.TestConstruct', version: localCdkVersion() } +} + +/** + * The exact values we expect from testing against version numbers in this suite depend on whether we're running + * on a development or release branch. Returns the local package.json version, which will be '0.0.0' unless we're + * on a release branch, in which case it should be the real version numbers (e.g., 1.91.0). + */ +function localCdkVersion(): string { + if (!_cdkVersion) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + _cdkVersion = require(path.join('..', 'package.json')).version; + if (!_cdkVersion) { + throw new Error('Unable to determine CDK version'); + } + } + return _cdkVersion; +} diff --git a/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts b/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts index 657c3c8fcc9c6..169b27782b841 100644 --- a/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts +++ b/packages/@aws-cdk/custom-resources/lib/provider-framework/provider.ts @@ -184,7 +184,7 @@ export class Provider extends Construct implements cfn.ICustomResourceProvider { const fn = new lambda.Function(this, `framework-${entrypoint}`, { code: lambda.Code.fromAsset(RUNTIME_HANDLER_PATH), description: `AWS CDK resource provider framework - ${entrypoint} (${this.node.path})`.slice(0, 256), - runtime: lambda.Runtime.NODEJS_10_X, + runtime: lambda.Runtime.NODEJS_14_X, handler: `framework.${entrypoint}`, timeout: FRAMEWORK_HANDLER_TIMEOUT, logRetention: this.logRetention, diff --git a/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json b/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json index e2f123073b19e..b9a9edfe958cc 100644 --- a/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json +++ b/packages/@aws-cdk/custom-resources/test/provider-framework/integ.provider.expected.json @@ -88,7 +88,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersfd43c0b4c0b961743009872f92ea51e922be342a344c19b9a8dd693f494d8da2S3BucketD831F708" + "Ref": "AssetParameters192597c3e09c72bcb5fca6899fca0b42745cb003a702e275a7f96123a9baf590S3BucketF43FE553" }, "S3Key": { "Fn::Join": [ @@ -101,7 +101,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersfd43c0b4c0b961743009872f92ea51e922be342a344c19b9a8dd693f494d8da2S3VersionKeyEAC17D61" + "Ref": "AssetParameters192597c3e09c72bcb5fca6899fca0b42745cb003a702e275a7f96123a9baf590S3VersionKey1CA1DD38" } ] } @@ -114,7 +114,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersfd43c0b4c0b961743009872f92ea51e922be342a344c19b9a8dd693f494d8da2S3VersionKeyEAC17D61" + "Ref": "AssetParameters192597c3e09c72bcb5fca6899fca0b42745cb003a702e275a7f96123a9baf590S3VersionKey1CA1DD38" } ] } @@ -124,13 +124,13 @@ ] } }, - "Handler": "index.onEvent", "Role": { "Fn::GetAtt": [ "comamazonawscdkcustomresourcess3fileproviders3fileoneventServiceRole999CEEB6", "Arn" ] }, + "Handler": "index.onEvent", "Runtime": "nodejs10.x" }, "DependsOn": [ @@ -200,7 +200,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3Bucket0EEA1C2E" + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" }, "S3Key": { "Fn::Join": [ @@ -213,7 +213,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3VersionKey7BCE18C9" + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" } ] } @@ -226,7 +226,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3VersionKey7BCE18C9" + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" } ] } @@ -236,14 +236,12 @@ ] } }, - "Handler": "framework.onEvent", "Role": { "Fn::GetAtt": [ "comamazonawscdkcustomresourcess3fileproviderframeworkonEventServiceRoleABFCDA11", "Arn" ] }, - "Runtime": "nodejs10.x", "Description": "AWS CDK resource provider framework - onEvent (integ-provider-framework/com.amazonaws.cdk.custom-resources.s3file-provider/s3file-provider)", "Environment": { "Variables": { @@ -255,6 +253,8 @@ } } }, + "Handler": "framework.onEvent", + "Runtime": "nodejs14.x", "Timeout": 900 }, "DependsOn": [ @@ -377,13 +377,13 @@ ] } }, - "Handler": "index.on_event", "Role": { "Fn::GetAtt": [ "comamazonawscdkcustomresourcess3assertproviders3assertoneventServiceRole012C0033", "Arn" ] }, + "Handler": "index.on_event", "Runtime": "python3.7" }, "DependsOn": [ @@ -487,13 +487,13 @@ ] } }, - "Handler": "index.is_complete", "Role": { "Fn::GetAtt": [ "comamazonawscdkcustomresourcess3assertproviders3assertiscompleteServiceRoleACAA755A", "Arn" ] }, + "Handler": "index.is_complete", "Runtime": "python3.7" }, "DependsOn": [ @@ -580,7 +580,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3Bucket0EEA1C2E" + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" }, "S3Key": { "Fn::Join": [ @@ -593,7 +593,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3VersionKey7BCE18C9" + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" } ] } @@ -606,7 +606,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3VersionKey7BCE18C9" + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" } ] } @@ -616,14 +616,12 @@ ] } }, - "Handler": "framework.onEvent", "Role": { "Fn::GetAtt": [ "comamazonawscdkcustomresourcess3assertproviderframeworkonEventServiceRole34070F2C", "Arn" ] }, - "Runtime": "nodejs10.x", "Description": "AWS CDK resource provider framework - onEvent (integ-provider-framework/com.amazonaws.cdk.custom-resources.s3assert-provider/s3assert-provider)", "Environment": { "Variables": { @@ -644,6 +642,8 @@ } } }, + "Handler": "framework.onEvent", + "Runtime": "nodejs14.x", "Timeout": 900 }, "DependsOn": [ @@ -723,7 +723,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3Bucket0EEA1C2E" + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" }, "S3Key": { "Fn::Join": [ @@ -736,7 +736,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3VersionKey7BCE18C9" + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" } ] } @@ -749,7 +749,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3VersionKey7BCE18C9" + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" } ] } @@ -759,14 +759,12 @@ ] } }, - "Handler": "framework.isComplete", "Role": { "Fn::GetAtt": [ "comamazonawscdkcustomresourcess3assertproviderframeworkisCompleteServiceRole2C8C7983", "Arn" ] }, - "Runtime": "nodejs10.x", "Description": "AWS CDK resource provider framework - isComplete (integ-provider-framework/com.amazonaws.cdk.custom-resources.s3assert-provider/s3assert-provider)", "Environment": { "Variables": { @@ -784,6 +782,8 @@ } } }, + "Handler": "framework.isComplete", + "Runtime": "nodejs14.x", "Timeout": 900 }, "DependsOn": [ @@ -863,7 +863,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3Bucket0EEA1C2E" + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1" }, "S3Key": { "Fn::Join": [ @@ -876,7 +876,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3VersionKey7BCE18C9" + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" } ] } @@ -889,7 +889,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3VersionKey7BCE18C9" + "Ref": "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F" } ] } @@ -899,14 +899,12 @@ ] } }, - "Handler": "framework.onTimeout", "Role": { "Fn::GetAtt": [ "comamazonawscdkcustomresourcess3assertproviderframeworkonTimeoutServiceRole15F6DFA2", "Arn" ] }, - "Runtime": "nodejs10.x", "Description": "AWS CDK resource provider framework - onTimeout (integ-provider-framework/com.amazonaws.cdk.custom-resources.s3assert-provider/s3assert-provider)", "Environment": { "Variables": { @@ -924,6 +922,8 @@ } } }, + "Handler": "framework.onTimeout", + "Runtime": "nodejs14.x", "Timeout": 900 }, "DependsOn": [ @@ -1034,29 +1034,29 @@ } }, "Parameters": { - "AssetParametersfd43c0b4c0b961743009872f92ea51e922be342a344c19b9a8dd693f494d8da2S3BucketD831F708": { + "AssetParameters192597c3e09c72bcb5fca6899fca0b42745cb003a702e275a7f96123a9baf590S3BucketF43FE553": { "Type": "String", - "Description": "S3 bucket for asset \"fd43c0b4c0b961743009872f92ea51e922be342a344c19b9a8dd693f494d8da2\"" + "Description": "S3 bucket for asset \"192597c3e09c72bcb5fca6899fca0b42745cb003a702e275a7f96123a9baf590\"" }, - "AssetParametersfd43c0b4c0b961743009872f92ea51e922be342a344c19b9a8dd693f494d8da2S3VersionKeyEAC17D61": { + "AssetParameters192597c3e09c72bcb5fca6899fca0b42745cb003a702e275a7f96123a9baf590S3VersionKey1CA1DD38": { "Type": "String", - "Description": "S3 key for asset version \"fd43c0b4c0b961743009872f92ea51e922be342a344c19b9a8dd693f494d8da2\"" + "Description": "S3 key for asset version \"192597c3e09c72bcb5fca6899fca0b42745cb003a702e275a7f96123a9baf590\"" }, - "AssetParametersfd43c0b4c0b961743009872f92ea51e922be342a344c19b9a8dd693f494d8da2ArtifactHashDD841113": { + "AssetParameters192597c3e09c72bcb5fca6899fca0b42745cb003a702e275a7f96123a9baf590ArtifactHashBBCB8F76": { "Type": "String", - "Description": "Artifact hash for asset \"fd43c0b4c0b961743009872f92ea51e922be342a344c19b9a8dd693f494d8da2\"" + "Description": "Artifact hash for asset \"192597c3e09c72bcb5fca6899fca0b42745cb003a702e275a7f96123a9baf590\"" }, - "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3Bucket0EEA1C2E": { + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3BucketDC4B98B1": { "Type": "String", - "Description": "S3 bucket for asset \"956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3\"" + "Description": "S3 bucket for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" }, - "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3S3VersionKey7BCE18C9": { + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1S3VersionKeyA495226F": { "Type": "String", - "Description": "S3 key for asset version \"956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3\"" + "Description": "S3 key for asset version \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" }, - "AssetParameters956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3ArtifactHash2CBB11D2": { + "AssetParametersdaeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1ArtifactHashA521A16F": { "Type": "String", - "Description": "Artifact hash for asset \"956c2f92ddbde06f551fdf914445c679dcadb21c6e8d1ee9c9632144ef5a2ad3\"" + "Description": "Artifact hash for asset \"daeb79e3cee39c9b902dc0d5c780223e227ed573ea60976252947adab5fb2be1\"" }, "AssetParameters4bafad8d010ba693e235b77d2c6decfc2ac79a8208d4477cbb36d31caf7189e8S3Bucket0DB889DF": { "Type": "String", diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index 15e6da27613d3..5413b38fa0d16 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -432,7 +432,7 @@ the `additionalArtifacts` property. Here are some typical examples for how you might want to bring in additional files from several sources: -* Directoy from the source repository +* Directory from the source repository * Additional compiled artifacts from the synth step ### Controlling IAM permissions @@ -523,7 +523,7 @@ const validationAction = new ShellScriptAction({ }); ``` -#### Add Additional permissions to the CodeBuild Project Role for building and synthing +#### Add Additional permissions to the CodeBuild Project Role for building and synthesizing You can customize the role permissions used by the CodeBuild project so it has access to the needed resources. eg: Adding CodeArtifact repo permissions so we pull npm packages @@ -677,7 +677,7 @@ contains: assets in these storage locations *without* the use of CloudFormation template parameters. * A set of roles with permissions to access these asset locations and to execute - CloudFormation, assumeable from whatever accounts you specify under `--trust`. + CloudFormation, assumable from whatever accounts you specify under `--trust`. It is possible and safe to migrate from the old bootstrap stack to the new bootstrap stack. This will create a new S3 file asset bucket in your account diff --git a/packages/@aws-cdk/region-info/build-tools/aws-entities.ts b/packages/@aws-cdk/region-info/build-tools/aws-entities.ts index 1588af66c6384..28c59828f7477 100644 --- a/packages/@aws-cdk/region-info/build-tools/aws-entities.ts +++ b/packages/@aws-cdk/region-info/build-tools/aws-entities.ts @@ -4,32 +4,33 @@ * Not in the list ==> no built-in data for that region. */ export const AWS_REGIONS = [ - 'us-east-2', - 'us-east-1', - 'us-west-1', - 'us-west-2', - 'us-gov-east-1', - 'us-gov-west-1', - 'us-iso-east-1', - 'us-isob-east-1', - 'af-south-1', - 'ap-east-1', - 'ap-south-1', - 'ap-northeast-2', - 'ap-southeast-1', - 'ap-southeast-2', - 'ap-northeast-1', - 'ca-central-1', - 'cn-north-1', - 'cn-northwest-1', - 'eu-central-1', - 'eu-west-1', - 'eu-west-2', - 'eu-west-3', - 'eu-north-1', - 'eu-south-1', - 'me-south-1', - 'sa-east-1', + 'af-south-1', // Africa (Cape Town) + 'ap-east-1', // Asia Pacific (Hong Kong) + 'ap-northeast-1', // Asia Pacific (Tokyo) + 'ap-northeast-2', // Asia Pacific (Seoul) + 'ap-northeast-3', // Asia Pacific (Osaka) + 'ap-south-1', // Asia Pacific (Mumbai) + 'ap-southeast-1', // Asia Pacific (Singapore) + 'ap-southeast-2', // Asia Pacific (Sydney) + 'ca-central-1', // Canada (Central) + 'cn-north-1', // China (Beijing) + 'cn-northwest-1', // China (Ningxia) + 'eu-central-1', // Europe (Frankfurt) + 'eu-north-1', // Europe (Stockholm) + 'eu-south-1', // Europe (Milan) + 'eu-west-1', // Europe (Ireland) + 'eu-west-2', // Europe (London) + 'eu-west-3', // Europe (Paris) + 'me-south-1', // Middle East (Bahrain) + 'sa-east-1', // South America (São Paulo) + 'us-east-1', // US East (N. Virginia) + 'us-east-2', // US East (Ohio) + 'us-gov-east-1', // AWS GovCloud (US-East) + 'us-gov-west-1', // AWS GovCloud (US-West) + 'us-iso-east-1', // AWS ISO + 'us-isob-east-1', // AWS ISO-B + 'us-west-1', // US West (N. California) + 'us-west-2', // US West (Oregon) ].sort(); /** diff --git a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts index dc7f8ff586449..c2ce689f3aaf3 100644 --- a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts +++ b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts @@ -19,7 +19,7 @@ export const AWS_CDK_METADATA = new Set([ // 'us-gov-west-1', // 'us-iso-east-1', // 'us-isob-east-1', - // 'af-south-1', + 'af-south-1', 'ap-south-1', 'ap-east-1', // 'ap-northeast-3', @@ -35,7 +35,7 @@ export const AWS_CDK_METADATA = new Set([ 'eu-west-2', 'eu-west-3', 'eu-north-1', - // 'eu-south-1', + 'eu-south-1', 'me-south-1', 'sa-east-1', ]); @@ -46,29 +46,29 @@ export const AWS_CDK_METADATA = new Set([ * @see https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_website_region_endpoints */ export const ROUTE_53_BUCKET_WEBSITE_ZONE_IDS: { [region: string]: string } = { - 'us-east-2': 'Z2O1EMRO9K5GLX', - 'us-east-1': 'Z3AQBSTGFYJSTF', - 'us-west-1': 'Z2F56UZL2M1ACD', - 'us-west-2': 'Z3BJ6K6RIION7M', - 'us-gov-east-1': 'Z2NIFVYYW2VKV1', - 'us-gov-west-1': 'Z31GFT0UA1I2HV', 'af-south-1': 'Z11KHD8FBVPUYU', 'ap-east-1': 'ZNB98KWMFR0R6', - 'ap-south-1': 'Z11RGJOFQNVJUP', - 'ap-northeast-3': 'Z2YQB5RD63NC85', + 'ap-northeast-1': 'Z2M4EHUR26P7ZW', 'ap-northeast-2': 'Z3W03O7B5YMIYP', + 'ap-northeast-3': 'Z2YQB5RD63NC85', + 'ap-south-1': 'Z11RGJOFQNVJUP', 'ap-southeast-1': 'Z3O0J2DXBE1FTB', 'ap-southeast-2': 'Z1WCIGYICN2BYD', - 'ap-northeast-1': 'Z2M4EHUR26P7ZW', 'ca-central-1': 'Z1QDHH18159H29', 'eu-central-1': 'Z21DNDUVLTQW6Q', + 'eu-north-1': 'Z3BAZG2TWCNX0D', + 'eu-south-1': 'Z3IXVV8C73GIO3', 'eu-west-1': 'Z1BKCTXD74EZPE', 'eu-west-2': 'Z3GKZC51ZF0DB4', 'eu-west-3': 'Z3R1K369G5AVDG', - 'eu-north-1': 'Z3BAZG2TWCNX0D', - 'eu-south-1': 'Z3IXVV8C73GIO3', - 'sa-east-1': 'Z7KQH4QJS55SO', 'me-south-1': 'Z1MPMWCPA7YB62', + 'sa-east-1': 'Z7KQH4QJS55SO', + 'us-east-1': 'Z3AQBSTGFYJSTF', + 'us-east-2': 'Z2O1EMRO9K5GLX', + 'us-gov-east-1': 'Z2NIFVYYW2VKV1', + 'us-gov-west-1': 'Z31GFT0UA1I2HV', + 'us-west-1': 'Z2F56UZL2M1ACD', + 'us-west-2': 'Z3BJ6K6RIION7M', }; interface Region { partition: string, domainSuffix: string } @@ -83,63 +83,64 @@ export const PARTITION_MAP: { [region: string]: Region } = { // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html#access-logging-bucket-permissions export const ELBV2_ACCOUNTS: { [region: string]: string } = { - 'us-east-1': '127311923021', - 'us-east-2': '033677994240', - 'us-west-1': '027434742980', - 'us-west-2': '797873946194', 'af-south-1': '098369216593', - 'ca-central-1': '985666609251', - 'eu-central-1': '054676820928', - 'eu-west-1': '156460612806', - 'eu-west-2': '652711504416', - 'eu-west-3': '009996457667', - 'eu-south-1': '635631232127', - 'eu-north-1': '897822967062', 'ap-east-1': '754344448648', 'ap-northeast-1': '582318560864', 'ap-northeast-2': '600734575887', 'ap-northeast-3': '383597477331', + 'ap-south-1': '718504428378', 'ap-southeast-1': '114774131450', 'ap-southeast-2': '783225319266', - 'ap-south-1': '718504428378', + 'ca-central-1': '985666609251', + 'cn-north-1': '638102146993', + 'cn-northwest-1': '037604701340', + 'eu-central-1': '054676820928', + 'eu-north-1': '897822967062', + 'eu-south-1': '635631232127', + 'eu-west-1': '156460612806', + 'eu-west-2': '652711504416', + 'eu-west-3': '009996457667', 'me-south-1': '076674570225', 'sa-east-1': '507241528517', - 'us-gov-west-1': '048591011584', + 'us-east-1': '127311923021', + 'us-east-2': '033677994240', 'us-gov-east-1': '190560391635', - 'cn-north-1': '638102146993', - 'cn-northwest-1': '037604701340', + 'us-gov-west-1': '048591011584', + 'us-west-1': '027434742980', + 'us-west-2': '797873946194', }; // https://aws.amazon.com/releasenotes/available-deep-learning-containers-images export const DLC_REPOSITORY_ACCOUNTS: { [region: string]: string } = { - 'us-east-1': '763104351884', - 'us-east-2': '763104351884', - 'us-west-1': '763104351884', - 'us-west-2': '763104351884', - 'ca-central-1': '763104351884', - 'eu-west-1': '763104351884', - 'eu-west-2': '763104351884', - 'eu-west-3': '763104351884', - 'eu-central-1': '763104351884', - 'eu-north-1': '763104351884', - 'sa-east-1': '763104351884', - 'ap-south-1': '763104351884', + 'ap-east-1': '871362719292', 'ap-northeast-1': '763104351884', 'ap-northeast-2': '763104351884', + 'ap-south-1': '763104351884', 'ap-southeast-1': '763104351884', 'ap-southeast-2': '763104351884', - - 'ap-east-1': '871362719292', - 'me-south-1': '217643126080', - + 'ca-central-1': '763104351884', 'cn-north-1': '727897471807', 'cn-northwest-1': '727897471807', + 'eu-central-1': '763104351884', + 'eu-north-1': '763104351884', + 'eu-west-1': '763104351884', + 'eu-west-2': '763104351884', + 'eu-west-3': '763104351884', + 'me-south-1': '217643126080', + 'sa-east-1': '763104351884', + 'us-east-1': '763104351884', + 'us-east-2': '763104351884', + 'us-west-1': '763104351884', + 'us-west-2': '763104351884', }; // https://docs.aws.amazon.com/app-mesh/latest/userguide/envoy.html export const APPMESH_ECR_ACCOUNTS: { [region: string]: string } = { + 'af-south-1': '924023996002', + 'ap-east-1': '856666278305', 'ap-northeast-1': '840364872350', 'ap-northeast-2': '840364872350', + 'ap-northeast-3': '840364872350', 'ap-south-1': '840364872350', 'ap-southeast-1': '840364872350', 'ap-southeast-2': '840364872350', @@ -150,14 +151,10 @@ export const APPMESH_ECR_ACCOUNTS: { [region: string]: string } = { 'eu-west-1': '840364872350', 'eu-west-2': '840364872350', 'eu-west-3': '840364872350', + 'me-south-1': '772975370895', 'sa-east-1': '840364872350', 'us-east-1': '840364872350', 'us-east-2': '840364872350', 'us-west-1': '840364872350', 'us-west-2': '840364872350', - - 'me-south-1': '772975370895', - 'ap-east-1': '856666278305', - 'af-south-1': '924023996002', - }; diff --git a/packages/@aws-cdk/region-info/build-tools/generate-static-data.ts b/packages/@aws-cdk/region-info/build-tools/generate-static-data.ts index 08a9f79a72b2b..d23704b6d0062 100644 --- a/packages/@aws-cdk/region-info/build-tools/generate-static-data.ts +++ b/packages/@aws-cdk/region-info/build-tools/generate-static-data.ts @@ -8,6 +8,11 @@ import { } from './fact-tables'; async function main(): Promise { + checkRegions(APPMESH_ECR_ACCOUNTS); + checkRegions(DLC_REPOSITORY_ACCOUNTS); + checkRegions(ELBV2_ACCOUNTS); + checkRegions(ROUTE_53_BUCKET_WEBSITE_ZONE_IDS); + const lines = [ "import { Fact, FactName } from './fact';", '', @@ -76,6 +81,19 @@ async function main(): Promise { } } +/** + * Verifies that the provided map of region to fact does not contain an entry + * for a region that was not registered in `AWS_REGIONS`. + */ +function checkRegions(map: Record) { + const allRegions = new Set(AWS_REGIONS); + for (const region of Object.keys(map)) { + if (!allRegions.has(region)) { + throw new Error(`Un-registered region fact found: ${region}. Add to AWS_REGIONS list!`); + } + } +} + main().catch(e => { // eslint-disable-next-line no-console console.error(e); diff --git a/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap b/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap index 55df3731f4eec..eb7c82b6eb28d 100644 --- a/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap +++ b/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap @@ -3,7 +3,7 @@ exports[`built-in data is correct 1`] = ` Object { "af-south-1": Object { - "cdkMetadataResourceAvailable": false, + "cdkMetadataResourceAvailable": true, "domainSuffix": "amazonaws.com", "partition": "aws", "s3StaticWebsiteEndpoint": "s3-website.af-south-1.amazonaws.com", @@ -82,6 +82,26 @@ Object { }, "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, + "ap-northeast-3": Object { + "cdkMetadataResourceAvailable": false, + "domainSuffix": "amazonaws.com", + "partition": "aws", + "s3StaticWebsiteEndpoint": "s3-website.ap-northeast-3.amazonaws.com", + "servicePrincipals": Object { + "application-autoscaling": "application-autoscaling.amazonaws.com", + "autoscaling": "autoscaling.amazonaws.com", + "codedeploy": "codedeploy.ap-northeast-3.amazonaws.com", + "ec2": "ec2.amazonaws.com", + "events": "events.amazonaws.com", + "lambda": "lambda.amazonaws.com", + "logs": "logs.ap-northeast-3.amazonaws.com", + "s3": "s3.amazonaws.com", + "sns": "sns.amazonaws.com", + "sqs": "sqs.amazonaws.com", + "states": "states.ap-northeast-3.amazonaws.com", + }, + "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", + }, "ap-south-1": Object { "cdkMetadataResourceAvailable": true, "domainSuffix": "amazonaws.com", @@ -243,7 +263,7 @@ Object { "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "eu-south-1": Object { - "cdkMetadataResourceAvailable": false, + "cdkMetadataResourceAvailable": true, "domainSuffix": "amazonaws.com", "partition": "aws", "s3StaticWebsiteEndpoint": "s3-website.eu-south-1.amazonaws.com", diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index a921736614acb..6424acdc74db3 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -49,16 +49,17 @@ "outdir": "dist", "targets": { "dotnet": { - "namespace": "Amazon.CDK.Lib", + "namespace": "Amazon.CDK", "packageId": "Amazon.CDK.Lib", "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png", "versionSuffix": "-devpreview" }, "java": { - "package": "software.amazon.awscdk.lib", + "package": "software.amazon.awscdk.core", "maven": { "groupId": "software.amazon.awscdk", - "artifactId": "lib" + "artifactId": "aws-cdk-lib", + "versionSuffix": ".DEVPREVIEW" } }, "python": { @@ -245,6 +246,7 @@ "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/aws-s3-deployment": "0.0.0", "@aws-cdk/aws-s3-notifications": "0.0.0", + "@aws-cdk/aws-s3outposts": "0.0.0", "@aws-cdk/aws-sagemaker": "0.0.0", "@aws-cdk/aws-sam": "0.0.0", "@aws-cdk/aws-sdb": "0.0.0", diff --git a/packages/decdk/package.json b/packages/decdk/package.json index aaf8a08e2ce97..8667de920b96c 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -36,8 +36,8 @@ "@aws-cdk/aws-amplify": "0.0.0", "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-apigatewayv2": "0.0.0", - "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", "@aws-cdk/aws-apigatewayv2-authorizers": "0.0.0", + "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", "@aws-cdk/aws-appconfig": "0.0.0", "@aws-cdk/aws-appflow": "0.0.0", "@aws-cdk/aws-applicationautoscaling": "0.0.0", @@ -174,6 +174,7 @@ "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/aws-s3-deployment": "0.0.0", "@aws-cdk/aws-s3-notifications": "0.0.0", + "@aws-cdk/aws-s3outposts": "0.0.0", "@aws-cdk/aws-sagemaker": "0.0.0", "@aws-cdk/aws-sam": "0.0.0", "@aws-cdk/aws-sdb": "0.0.0", diff --git a/packages/monocdk/package.json b/packages/monocdk/package.json index 44e2607a79ab1..7fed97261e30d 100644 --- a/packages/monocdk/package.json +++ b/packages/monocdk/package.json @@ -253,6 +253,7 @@ "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/aws-s3-deployment": "0.0.0", "@aws-cdk/aws-s3-notifications": "0.0.0", + "@aws-cdk/aws-s3outposts": "0.0.0", "@aws-cdk/aws-sagemaker": "0.0.0", "@aws-cdk/aws-sam": "0.0.0", "@aws-cdk/aws-sdb": "0.0.0", diff --git a/scripts/bump.js b/scripts/bump.js index 5d8b4d63a62d0..bcbe8785c436b 100755 --- a/scripts/bump.js +++ b/scripts/bump.js @@ -11,7 +11,7 @@ const forTesting = process.env.BUMP_CANDIDATE || false; async function main() { if (releaseAs !== 'minor' && releaseAs !== 'patch') { - throw new error(`invalid bump type "${releaseAs}". only "minor" (the default) and "patch" are allowed. major version bumps require *slightly* more intention`); + throw new Error(`invalid bump type "${releaseAs}". only "minor" (the default) and "patch" are allowed. major version bumps require *slightly* more intention`); } console.error(`Starting ${releaseAs} version bump`); diff --git a/scripts/resolve-version-lib.js b/scripts/resolve-version-lib.js index 2a7f0e4eecebc..21a13c0eb4ab2 100755 --- a/scripts/resolve-version-lib.js +++ b/scripts/resolve-version-lib.js @@ -38,7 +38,6 @@ function resolveVersion(rootdir) { // const currentVersion = require(versionFilePath).version; - console.error(`current version: ${currentVersion}`); if (!currentVersion.startsWith(`${majorVersion}.`)) { throw new Error(`current version "${currentVersion}" does not use the expected major version ${majorVersion}`); } diff --git a/version.v1.json b/version.v1.json index c2a1515792517..0a93d433950d0 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.92.0" + "version": "1.94.1" }