From 7711981ea30bfdffd21dd840d676be4a2b45c9ba Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Wed, 10 Mar 2021 10:38:11 +0100 Subject: [PATCH 01/20] feat(cfnspec): cloudformation spec v30.1.0 (#13519) Co-authored-by: AWS CDK Team --- packages/@aws-cdk/aws-s3outposts/.eslintrc.js | 3 + packages/@aws-cdk/aws-s3outposts/.gitignore | 19 + packages/@aws-cdk/aws-s3outposts/.npmignore | 28 + packages/@aws-cdk/aws-s3outposts/LICENSE | 201 +++ packages/@aws-cdk/aws-s3outposts/NOTICE | 2 + packages/@aws-cdk/aws-s3outposts/README.md | 20 + .../@aws-cdk/aws-s3outposts/jest.config.js | 2 + packages/@aws-cdk/aws-s3outposts/lib/index.ts | 2 + packages/@aws-cdk/aws-s3outposts/package.json | 100 ++ .../aws-s3outposts/test/s3outposts.test.ts | 6 + packages/@aws-cdk/cfnspec/CHANGELOG.md | 59 + packages/@aws-cdk/cfnspec/cfn.version | 2 +- ...0_CloudFormationResourceSpecification.json | 1112 ++++++++++++++++- .../cloudformation-include/package.json | 2 + packages/aws-cdk-lib/package.json | 1 + packages/decdk/package.json | 3 +- packages/monocdk/package.json | 1 + 17 files changed, 1547 insertions(+), 16 deletions(-) create mode 100644 packages/@aws-cdk/aws-s3outposts/.eslintrc.js create mode 100644 packages/@aws-cdk/aws-s3outposts/.gitignore create mode 100644 packages/@aws-cdk/aws-s3outposts/.npmignore create mode 100644 packages/@aws-cdk/aws-s3outposts/LICENSE create mode 100644 packages/@aws-cdk/aws-s3outposts/NOTICE create mode 100644 packages/@aws-cdk/aws-s3outposts/README.md create mode 100644 packages/@aws-cdk/aws-s3outposts/jest.config.js create mode 100644 packages/@aws-cdk/aws-s3outposts/lib/index.ts create mode 100644 packages/@aws-cdk/aws-s3outposts/package.json create mode 100644 packages/@aws-cdk/aws-s3outposts/test/s3outposts.test.ts 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..7965518ea1d24 --- /dev/null +++ b/packages/@aws-cdk/aws-s3outposts/package.json @@ -0,0 +1,100 @@ +{ + "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" + }, + "peerDependencies": { + "@aws-cdk/core": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "cfn-only", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + } +} 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/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/package.json b/packages/@aws-cdk/cloudformation-include/package.json index f9b2e79888365..a177759248e07 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -186,6 +186,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", @@ -334,6 +335,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-lib/package.json b/packages/aws-cdk-lib/package.json index 89dbc64979387..bd2ce09408acc 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -247,6 +247,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 8b8b17d52f621..4b2ec6dc2fdc0 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -35,8 +35,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", @@ -173,6 +173,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 c71d755b3c2bb..382f47c2be9d0 100644 --- a/packages/monocdk/package.json +++ b/packages/monocdk/package.json @@ -252,6 +252,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", From 8dca5079e1893122057f9e2c54c0da0ba644926e Mon Sep 17 00:00:00 2001 From: Nick Lynch Date: Wed, 10 Mar 2021 11:51:11 +0000 Subject: [PATCH 02/20] chore(core): update CDK Metadata to report construct-level details (#13423) See CDK RFC 253 (aws/aws-cdk-rfcs#254) for background and details. Currently -- if a user has not opted out -- an AWS::CDK::Metadata resource is added to each generated stack template with details about each loaded module and version that matches an Amazon-specific allow list. This modules list is used to: - Track what library versions customers are using so they can be contacted in the event of a severe (security) issue with a library. - Get business metrics on the adoption of CDK and its libraries. This modules list is sometimes inaccurate (a module may be loaded into memory without actually being used) and too braod to support CDK v2. This feature (mostly) implements the specification proposed in RFC 253 to include metadata about what constructs are present in each stack, rather than modules loaded into memory. The allow-list is still used to ensure only CDK/AWS constructs are reported on. Implementation notes: - The format of the Analytics property has changed slightly since the RFC. See the service-side code for justification and latest spec. - How to handle the jsii runtime information was left un-spec'd. I've chosen to create a psuedo-Construct to add to the list as the simplest solution. - `runtime-info.test.ts` leaps through some serious hoops to work equally well for both v1 and v2, and to fail somewhat gracefully locally if `tsc` was used to compile the module instead of `jsii`. Critques of this approach welcome! - I removed an annoyance from `resolve-version-lib.js` that produced error messages when running unit tests. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../core/lib/private/metadata-resource.ts | 121 ++++++--- .../@aws-cdk/core/lib/private/runtime-info.ts | 143 +++++----- .../core/lib/private/tree-metadata.ts | 30 +-- packages/@aws-cdk/core/test/app.test.ts | 124 --------- .../core/test/metadata-resource.test.ts | 151 +++++++++++ .../@aws-cdk/core/test/runtime-info.test.ts | 252 +++++++++++++----- scripts/resolve-version-lib.js | 1 - 7 files changed, 480 insertions(+), 342 deletions(-) create mode 100644 packages/@aws-cdk/core/test/metadata-resource.test.ts diff --git a/packages/@aws-cdk/core/lib/private/metadata-resource.ts b/packages/@aws-cdk/core/lib/private/metadata-resource.ts index ff84b931f819b..1cccc4f24ff3d 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 { CfnCondition } from '../cfn-condition'; import { Fn } from '../cfn-fn'; @@ -8,41 +8,12 @@ import { Construct } from '../construct-compat'; 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/runtime-info.ts b/packages/@aws-cdk/core/lib/private/runtime-info.ts index b0cf266e8e11d..da4fbdcbe99d8 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 '../construct-compat'; +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/tree-metadata.ts b/packages/@aws-cdk/core/lib/private/tree-metadata.ts index caa5c37a5940d..97fe514bb4d87 100644 --- a/packages/@aws-cdk/core/lib/private/tree-metadata.ts +++ b/packages/@aws-cdk/core/lib/private/tree-metadata.ts @@ -6,16 +6,10 @@ import { Annotations } from '../annotations'; import { Construct, IConstruct, ISynthesisSession } from '../construct-compat'; import { Stack } from '../stack'; 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. @@ -48,14 +42,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; @@ -96,16 +88,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; @@ -117,11 +99,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/test/app.test.ts b/packages/@aws-cdk/core/test/app.test.ts index 69486987f0085..199b36dc87465 100644 --- a/packages/@aws-cdk/core/test/app.test.ts +++ b/packages/@aws-cdk/core/test/app.test.ts @@ -4,7 +4,6 @@ import { nodeunitShim, Test } from 'nodeunit-shim'; import { CfnResource, Construct, 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({ @@ -260,90 +259,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) => { @@ -420,42 +335,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/metadata-resource.test.ts b/packages/@aws-cdk/core/test/metadata-resource.test.ts new file mode 100644 index 0000000000000..2275bcf7dee9d --- /dev/null +++ b/packages/@aws-cdk/core/test/metadata-resource.test.ts @@ -0,0 +1,151 @@ +import * as zlib from 'zlib'; +import { App, Stack } from '../lib'; +import { formatAnalytics } from '../lib/private/metadata-resource'; + +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '../lib'; +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('1.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: '1.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..7da4f78d74b46 100644 --- a/packages/@aws-cdk/core/test/runtime-info.test.ts +++ b/packages/@aws-cdk/core/test/runtime-info.test.ts @@ -1,73 +1,189 @@ -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'; - -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'; - - 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, - })); - - // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies - require(pkgdir); - - const runtimeInfo = collectRuntimeInformation(); - - // eslint-disable-next-line @typescript-eslint/no-require-imports - test.deepEqual(runtimeInfo.libraries['@aws-solutions-konstruk/foo'], mockVersion); - test.done(); - }, - - 'version reporting finds aws-rfdk package'(test: Test) { - const pkgdir = fs.mkdtempSync(path.join(os.tmpdir(), 'runtime-info-rfdk')); - const mockVersion = '1.2.3'; - - 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, - })); +import { App, NestedStack, Stack, Stage } from '../lib'; +import { constructInfoFromConstruct, constructInfoFromStack } from '../lib/private/runtime-info'; + +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '../lib'; + +const JSII_RUNTIME_SYMBOL = Symbol.for('jsii.rtti'); + +let app: App; +let stack: Stack; +let _cdkVersion: string | undefined = undefined; +const modulePrefix = cdkMajorVersion() === 1 ? '@aws-cdk/core' : 'aws-cdk-lib'; + +// 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, + }); +}); - // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies - require(pkgdir); +describeTscSafe('constructInfoFromConstruct', () => { + test('returns fqn and version for core constructs', () => { + const constructInfo = constructInfoFromConstruct(stack); + expect(constructInfo).toBeDefined(); + expect(constructInfo?.fqn).toEqual(`${modulePrefix}.Stack`); + expect(constructInfo?.version).toEqual(localCdkVersion()); + }); + + test('returns base construct info if no more specific info is present', () => { + const simpleConstruct = new class extends Construct { }(stack, 'Simple'); + const constructInfo = constructInfoFromConstruct(simpleConstruct); + expect(constructInfo?.fqn).toEqual(`${modulePrefix}.Construct`); + }); + + test('returns more specific subclass info if present', () => { + const construct = new class extends Construct { + // @ts-ignore + private static readonly [JSII_RUNTIME_SYMBOL] = { fqn: 'aws-cdk-lib.TestConstruct', version: localCdkVersion() } + }(stack, 'TestConstruct'); + + const constructInfo = constructInfoFromConstruct(construct); + expect(constructInfo?.fqn).toEqual('aws-cdk-lib.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); + }); + }); +}); - const runtimeInfo = collectRuntimeInformation(); +describeTscSafe('constructInfoForStack', () => { + test('returns stack itself and jsii runtime if stack is empty', () => { + const constructInfos = constructInfoFromStack(stack); + + expect(constructInfos.length).toEqual(2); + + const stackInfo = constructInfos.find(i => /Stack/.test(i.fqn)); + const jsiiInfo = constructInfos.find(i => i.fqn === 'jsii-runtime.Runtime'); + expect(stackInfo?.fqn).toEqual(`${modulePrefix}.Stack`); + expect(stackInfo?.version).toEqual(localCdkVersion()); + expect(jsiiInfo?.version).toMatch(/node.js/); + }); + + test('returns info for constructs added to the stack', () => { + new class extends Construct { }(stack, 'Simple'); + + const constructInfos = constructInfoFromStack(stack); + + expect(constructInfos.length).toEqual(3); + expect(constructInfos.map(info => info.fqn)).toContain(`${modulePrefix}.Construct`); + }); + + test('returns unique info (no duplicates)', () => { + new class extends Construct { }(stack, 'Simple1'); + new class extends Construct { }(stack, 'Simple2'); + + const constructInfos = constructInfoFromStack(stack); + + expect(constructInfos.length).toEqual(3); + expect(constructInfos.map(info => info.fqn)).toContain(`${modulePrefix}.Construct`); + }); + + test('returns info from nested constructs', () => { + new class extends Construct { + constructor(scope: Construct, id: string) { + super(scope, id); + return new class extends Construct { + // @ts-ignore + private static readonly [JSII_RUNTIME_SYMBOL] = { fqn: '@aws-cdk/test.TestV1Construct', version: localCdkVersion() } + }(this, 'TestConstruct'); + } + }(stack, 'Nested'); + + const constructInfos = constructInfoFromStack(stack); + + expect(constructInfos.length).toEqual(4); + expect(constructInfos.map(info => info.fqn)).toContain('@aws-cdk/test.TestV1Construct'); + }); + + test('does not return info from nested stacks', () => { + new class extends Construct { + constructor(scope: Construct, id: string) { + super(scope, id); + + new class extends Construct { + // @ts-ignore + private static readonly [JSII_RUNTIME_SYMBOL] = { fqn: '@aws-cdk/test.TestV1Construct', version: localCdkVersion() } + }(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.TestV1Construct'); + expect(fqns).not.toContain('@aws-cdk/test.TestStackInsideStack'); + expect(fqns).not.toContain('@aws-cdk/test.TestNestedStackInsideStack'); + expect(fqns).not.toContain('@aws-cdk/test.TestStageInsideStack'); + }); +}); +function cdkMajorVersion(): number { + // eslint-disable-next-line @typescript-eslint/no-require-imports + return require('../../../../release.json').majorVersion; +} + +/** + * 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 - 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(); - }, -}); + _cdkVersion = require(path.join('..', 'package.json')).version; + if (!_cdkVersion) { + throw new Error('Unable to determine CDK version'); + } + } + return _cdkVersion; +} 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}`); } From bc1293b8062a0792d6d1d9f058f308c72475e778 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 10 Mar 2021 13:25:51 +0100 Subject: [PATCH 03/20] refactor(core): refactor `CloudFormationLang.toJSON()` (#11224) Our previous implementation of `toJSON()` was quite hacky. It replaced values inside the structure with objects that had a custom `toJSON()` serializer, and then called `JSON.stringify()` on the result. The resulting JSON would have special markers in it where the Token values would be string-substituted back in. It's actually easier and gives us more control to just implement JSONification ourselves in a Token-aware recursive function. This change has been split off from a larger, upcoming PR in order to make the individual reviews smaller. Incidentally also fixes #13465, as the type of encoded tokens is assumed to match the type of the encoded value (e.g., a `string[]`-encoded token is assumed to produce a list at deploy-time and so will not be quoted). ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../core/lib/private/cloudformation-lang.ts | 346 ++++++++++++++---- packages/@aws-cdk/core/lib/private/resolve.ts | 137 ++++++- .../@aws-cdk/core/lib/private/token-map.ts | 3 +- packages/@aws-cdk/core/lib/resolvable.ts | 14 +- .../core/test/cloudformation-json.test.ts | 329 ++++++++++++----- packages/@aws-cdk/core/test/evaluate-cfn.ts | 14 +- 6 files changed, 645 insertions(+), 198 deletions(-) diff --git a/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts b/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts index 4a74665b8f338..310a4632f4e8f 100644 --- a/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts +++ b/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts @@ -1,10 +1,7 @@ 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 { Token } from '../token'; -import { Intrinsic } from './intrinsic'; -import { resolve } from './resolve'; +import { INTRINSIC_KEY_PREFIX, ResolutionTypeHint, resolvedTypeHint } from './resolve'; /** * Routines that know how to do operations at the CloudFormation document language level @@ -24,59 +21,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 +47,227 @@ 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)) { + return renderIntrinsic(obj); + } + + 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('"'); + break; + + default: + pushIntrinsic(intrinsic); + break; + } + } + + /** + * 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 +337,58 @@ 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); +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/private/resolve.ts b/packages/@aws-cdk/core/lib/private/resolve.ts index d6ae73cdb8796..5f9620ecb759c 100644 --- a/packages/@aws-cdk/core/lib/private/resolve.ts +++ b/packages/@aws-cdk/core/lib/private/resolve.ts @@ -1,5 +1,5 @@ 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'; @@ -9,9 +9,38 @@ import { TokenMap } from './token-map'; import { IConstruct as ICoreConstruct } from '../construct-compat'; // 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 * @@ -25,6 +54,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; } /** @@ -50,7 +109,7 @@ export function resolve(obj: any, options: IResolveOptions): any { preparing: options.preparing, scope: options.scope as ICoreConstruct, 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; } }]; @@ -98,7 +157,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; } @@ -107,7 +166,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); } // @@ -124,7 +183,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 @@ -140,7 +199,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; } // @@ -155,24 +215,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; } /** @@ -222,3 +298,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/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/resolvable.ts b/packages/@aws-cdk/core/lib/resolvable.ts index 2ddbd544ffbbb..9004cd111bb33 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/cloudformation-json.test.ts b/packages/@aws-cdk/core/test/cloudformation-json.test.ts index e9d850eb178a2..cb96020e04904 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,104 @@ 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":', { Ref: 'Thing' }, '}']], + }); + }); - 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 +157,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 +176,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 +191,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 +235,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 +262,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 +276,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 +288,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 +306,7 @@ nodeunitShim({ // THEN const asm = app.synth(); - test.deepEqual(asm.getStackByName('Stack2').template, { + expect(asm.getStackByName('Stack2').template).toEqual({ Outputs: { Stack1Id: { Value: { @@ -232,11 +321,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 +362,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 +370,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 +402,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; } From e635dac49c66773cd82fd260e6747469728ffbc1 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Wed, 10 Mar 2021 08:33:42 -0800 Subject: [PATCH 04/20] chore: change the parameter used for 'find' in link-all.sh (#13510) The parameter currently used for `find` in `link-all.sh`, `-perm /111`, fails on my Mac. Switch to using `-perm +111`, which works fine, and that's also what JSII uses in [its `link-all.sh` script](https://github.com/aws/jsii/blob/f8bde4a01bf7c707c87ab00748eeeb7632e7c820/scripts/link-all.sh#L26-L26). ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- link-all.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 8d592ea89c0eda19329d5a31517522ec02ceb874 Mon Sep 17 00:00:00 2001 From: Darren Date: Wed, 10 Mar 2021 11:29:38 -0600 Subject: [PATCH 05/20] fix(iam): policy statement tries to validate tokens (#13493) Looking for guidance on error messaging and/or docs to update Fixes #13479 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-iam/lib/policy-statement.ts | 3 ++- .../@aws-cdk/aws-iam/test/policy-document.test.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) 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({ From 78b265cba23b438cb53655dee40b286852b9b3b7 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 10 Mar 2021 19:04:52 +0100 Subject: [PATCH 06/20] chore(aws-cdk-lib): change namespaces/package names in line with RFC 6 (#13494) Changes: * .NET: Namespace changed from `Amazon.CDK.Lib` -> `Amazon.CDK` (so `Stack` has the same FQN, same namespace as in Monocdk) * Java: Package name changed from `software.amazon.awscdk.lib` -> `software.amazon.awscdk.core` (so `Stack` has the same FQN, same namespace as in Monocdk) * Java: Changed artifact ID to match what's written in [RFC 6] * Python: Changed dist name to match what's written in [RFC 6] * Python: Change namespace to `aws_cdk` instead of `aws_cdk_lib` for minimal interference. Still need to test whether it's okay to change this to `aws_cdk.core` (like for Java) so `Stack` will keep the same FQN. Monocdk does something different for Python. [RFC 6]: https://github.com/aws/aws-cdk-rfcs/blob/master/text/0006-monolothic-packaging.md ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk-lib/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index bd2ce09408acc..5380837857d8f 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -50,22 +50,22 @@ "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": { - "distName": "aws-cdk.lib", - "module": "aws_cdk.lib" + "distName": "aws-cdk-lib", + "module": "aws_cdk" } }, "projectReferences": false From cc608d055ffefb798ad6378ab07f36cb241897da Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Thu, 11 Mar 2021 00:08:21 +0530 Subject: [PATCH 07/20] feat(stepfunctions-tasks): Support calling ApiGateway REST and HTTP APIs (#13033) feat(stepfunctions-tasks): Support calling APIGW REST and HTTP APIs Taking ownership of the original PR #11565 by @Sumeet-Badyal API as per documentation here: https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-control-access-using-iam-policies-to-invoke-api.html closes #11566 closes #11565 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-stepfunctions-tasks/README.md | 44 ++ .../lib/apigateway/base-types.ts | 79 ++++ .../lib/apigateway/base.ts | 69 +++ .../lib/apigateway/call-http-api.ts | 62 +++ .../lib/apigateway/call-rest-api.ts | 51 +++ .../lib/apigateway/index.ts | 3 + .../aws-stepfunctions-tasks/lib/index.ts | 1 + .../aws-stepfunctions-tasks/package.json | 6 + .../test/apigateway/call-http-api.test.ts | 145 +++++++ .../test/apigateway/call-rest-api.test.ts | 151 +++++++ .../integ.call-http-api.expected.json | 263 ++++++++++++ .../test/apigateway/integ.call-http-api.ts | 48 +++ .../integ.call-rest-api.expected.json | 394 ++++++++++++++++++ .../test/apigateway/integ.call-rest-api.ts | 43 ++ 14 files changed, 1359 insertions(+) create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base-types.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/base.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-http-api.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/call-rest-api.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/index.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-http-api.test.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/call-rest-api.test.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.expected.json create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-http-api.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.expected.json create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.call-rest-api.ts 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/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 a6137f570b1a3..b18cd8fc7704c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -72,6 +72,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", @@ -95,6 +98,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, +}); From 66f7053a6c1f5cab540e975b30f5a2c6e35df58a Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Wed, 10 Mar 2021 18:31:45 -0700 Subject: [PATCH 08/20] feat(appmesh): add route retry policies (#13353) Adds route retry policies for http/http2 and gRPC routes. Closes #11642 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appmesh/README.md | 44 +++ .../@aws-cdk/aws-appmesh/lib/route-spec.ts | 198 ++++++++++++ .../aws-appmesh/test/integ.mesh.expected.json | 129 +++++++- .../@aws-cdk/aws-appmesh/test/integ.mesh.ts | 26 ++ .../@aws-cdk/aws-appmesh/test/test.route.ts | 294 +++++++++++++++++- 5 files changed, 678 insertions(+), 13 deletions(-) diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index c400cbb0af05d..74aead1f02a02 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -320,6 +320,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. diff --git a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts index 74b16976b69ca..11f629c4aee91 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 * as cdk from '@aws-cdk/core'; import { CfnRoute } from './appmesh.generated'; import { Protocol, HttpTimeout, GrpcTimeout, TcpTimeout } from './shared-interfaces'; import { IVirtualNode } from './virtual-node'; @@ -68,6 +69,81 @@ 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: cdk.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', } /** @@ -107,6 +183,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', } /** @@ -203,12 +337,32 @@ class HttpRouteSpec extends RouteSpec { */ public readonly weightedTargets: WeightedTarget[]; + /** + * The retry policy + */ + public readonly retryPolicy?: HttpRetryPolicy; + constructor(props: HttpRouteSpecOptions, protocol: Protocol) { super(); this.protocol = protocol; this.match = props.match; this.weightedTargets = props.weightedTargets; this.timeout = props.timeout; + + 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 { @@ -216,6 +370,7 @@ class HttpRouteSpec extends RouteSpec { if (prefixPath[0] != '/') { throw new Error(`Prefix Path must start with \'/\', got: ${prefixPath}`); } + const httpConfig: CfnRoute.HttpRouteProperty = { action: { weightedTargets: renderWeightedTargets(this.weightedTargets), @@ -224,6 +379,7 @@ class HttpRouteSpec extends RouteSpec { prefix: prefixPath, }, timeout: renderTimeout(this.timeout), + retryPolicy: this.retryPolicy ? renderHttpRetryPolicy(this.retryPolicy) : undefined, }; return { httpRouteSpec: this.protocol === Protocol.HTTP ? httpConfig : undefined, @@ -266,11 +422,33 @@ class GrpcRouteSpec extends RouteSpec { 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; + + 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 { @@ -283,6 +461,7 @@ class GrpcRouteSpec extends RouteSpec { serviceName: this.match.serviceName, }, timeout: renderTimeout(this.timeout), + retryPolicy: this.retryPolicy ? renderGrpcRetryPolicy(this.retryPolicy) : undefined, }, }; } @@ -323,3 +502,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/test/integ.mesh.expected.json b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json index 5f4a9ca206725..f951953924b44 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,113 @@ "meshrouter81B8087E", "VirtualRouterName" ] - } + }, + "RouteName": "route-3" + } + }, + "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" + } + }, + "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 +937,6 @@ "meshgateway1gateway1routehttpE8D6F433": { "Type": "AWS::AppMesh::GatewayRoute", "Properties": { - "GatewayRouteName": "meshstackmeshgateway1gateway1routehttpBA921D42", "MeshName": { "Fn::GetAtt": [ "meshACDFE68E", @@ -863,13 +967,13 @@ "meshgateway1B02387E8", "VirtualGatewayName" ] - } + }, + "GatewayRouteName": "meshstackmeshgateway1gateway1routehttpBA921D42" } }, "meshgateway1gateway1routehttp2FD69C306": { "Type": "AWS::AppMesh::GatewayRoute", "Properties": { - "GatewayRouteName": "meshstackmeshgateway1gateway1routehttp255781963", "MeshName": { "Fn::GetAtt": [ "meshACDFE68E", @@ -900,13 +1004,13 @@ "meshgateway1B02387E8", "VirtualGatewayName" ] - } + }, + "GatewayRouteName": "meshstackmeshgateway1gateway1routehttp255781963" } }, "meshgateway1gateway1routegrpc76486062": { "Type": "AWS::AppMesh::GatewayRoute", "Properties": { - "GatewayRouteName": "meshstackmeshgateway1gateway1routegrpcCD4D891D", "MeshName": { "Fn::GetAtt": [ "meshACDFE68E", @@ -942,7 +1046,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..c1e909e38d75b 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -140,6 +140,32 @@ router.addRoute('route-3', { }), }); +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-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.route.ts b/packages/@aws-cdk/aws-appmesh/test/test.route.ts index cb5c92e6464cf..43c2d942a669b 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,298 @@ 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(); + }, }, 'Can import Routes using an ARN'(test: Test) { From 4c63f09f1e9644877eaffbe78eede3854bec08ab Mon Sep 17 00:00:00 2001 From: Janario Oliveira Date: Thu, 11 Mar 2021 07:39:45 +0100 Subject: [PATCH 09/20] feat(amplify-domain): Added config for auto subdomain creation (#13342) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-amplify/README.md | 5 +- packages/@aws-cdk/aws-amplify/lib/app.ts | 1 + packages/@aws-cdk/aws-amplify/lib/domain.ts | 24 ++++ .../@aws-cdk/aws-amplify/test/domain.test.ts | 111 ++++++++++++++++++ 4 files changed, 140 insertions(+), 1 deletion(-) 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 bf6ba7afe8017..f8683f44b123d 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 From e9cd1e84df3a99bca4ac98890c729f8dec899fd7 Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Thu, 11 Mar 2021 11:20:26 +0100 Subject: [PATCH 10/20] chore(test): make metadata resource test immune to encoding (#13538) The prefix-encoded trie could occasionally encode the tested resource name in a way that prevents the test to match. Using a "fake" version number ensures a unique prefix is always present, and hence the tested entry will never be encoded in unexpected ways. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/core/test/metadata-resource.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/core/test/metadata-resource.test.ts b/packages/@aws-cdk/core/test/metadata-resource.test.ts index 2275bcf7dee9d..00869746b1e25 100644 --- a/packages/@aws-cdk/core/test/metadata-resource.test.ts +++ b/packages/@aws-cdk/core/test/metadata-resource.test.ts @@ -63,7 +63,7 @@ describe('MetadataResource', () => { test('includes constructs added to the stack', () => { new TestConstruct(stack, 'Test'); - expect(stackAnalytics()).toContain('1.2.3!@amzn/core.TestConstruct'); + expect(stackAnalytics()).toContain('FakeVersion.2.3!@amzn/core.TestConstruct'); }); test('only includes constructs in the allow list', () => { @@ -141,11 +141,10 @@ 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: '1.2.3' } + 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' } } - From b5d4b923ea55a034b90eb7a30b0e647daf7524ec Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Thu, 11 Mar 2021 12:33:11 +0000 Subject: [PATCH 11/20] chore(release): 1.93.0 --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ version.v1.json | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fad4611dfad6c..ca6ebeb1b8572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,42 @@ 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.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. diff --git a/version.v1.json b/version.v1.json index c2a1515792517..097cc55f8cc18 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.92.0" + "version": "1.93.0" } From 77449f61e7075fef1240fc52becb8ea60b9ea9ad Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 11 Mar 2021 15:26:41 +0100 Subject: [PATCH 12/20] fix(lambda): fromDockerBuild output is located under /asset (#13539) Ensure `imagePath` ends with `/.` so that the content at that location is copied. See https://docs.docker.com/engine/reference/commandline/cp/ Closes #13439 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-lambda/lib/code.ts | 12 +++- .../@aws-cdk/aws-lambda/test/code.test.ts | 56 ++++++++++++++++++- .../test/docker-build-lambda/Dockerfile | 2 +- 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda/lib/code.ts b/packages/@aws-cdk/aws-lambda/lib/code.ts index b4f41b2804257..fec1e1821270e 100644 --- a/packages/@aws-cdk/aws-lambda/lib/code.ts +++ b/packages/@aws-cdk/aws-lambda/lib/code.ts @@ -67,9 +67,19 @@ export abstract class Code { * @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/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/ From b71efd9d12843ab4b495d53e565cec97d60748f3 Mon Sep 17 00:00:00 2001 From: Josh Kellendonk Date: Thu, 11 Mar 2021 16:55:21 -0700 Subject: [PATCH 13/20] feat(appmesh): add missing route match features (#13350) Adds route priority, header matching and matching by scheme and method. Closes #11645 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-appmesh/README.md | 24 ++ .../@aws-cdk/aws-appmesh/lib/route-spec.ts | 312 +++++++++++++++++- packages/@aws-cdk/aws-appmesh/lib/route.ts | 1 + .../aws-appmesh/test/integ.mesh.expected.json | 157 +++++++++ .../@aws-cdk/aws-appmesh/test/integ.mesh.ts | 30 ++ .../@aws-cdk/aws-appmesh/test/test.route.ts | 272 +++++++++++++++ 6 files changed, 778 insertions(+), 18 deletions(-) diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 74aead1f02a02..63203d6b365d1 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -298,6 +298,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 diff --git a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts index 11f629c4aee91..add785c02c286 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route-spec.ts @@ -35,6 +35,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, + }, + }; + } } /** @@ -47,10 +296,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 * @@ -149,7 +411,7 @@ export enum TcpRetryEvent { /** * 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 */ @@ -166,7 +428,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 */ @@ -274,6 +536,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; } /** @@ -317,24 +587,11 @@ 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; - /** - * List of targets that traffic is routed to when a request matches the route - */ public readonly weightedTargets: WeightedTarget[]; /** @@ -348,6 +605,7 @@ 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 ?? []; @@ -365,7 +623,7 @@ class HttpRouteSpec extends RouteSpec { } } - 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}`); @@ -377,11 +635,15 @@ class HttpRouteSpec extends RouteSpec { }, 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, }; @@ -389,6 +651,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 */ @@ -403,10 +670,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), @@ -418,6 +687,11 @@ 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; @@ -432,6 +706,7 @@ class GrpcRouteSpec extends RouteSpec { this.weightedTargets = props.weightedTargets; this.match = props.match; this.timeout = props.timeout; + this.priority = props.priority; if (props.retryPolicy) { const grpcRetryEvents = props.retryPolicy.grpcRetryEvents ?? []; @@ -453,6 +728,7 @@ class GrpcRouteSpec extends RouteSpec { public bind(_scope: Construct): RouteSpecConfig { return { + priority: this.priority, grpcRouteSpec: { action: { weightedTargets: renderWeightedTargets(this.weightedTargets), 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/test/integ.mesh.expected.json b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json index f951953924b44..c3139e2b75582 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -625,6 +625,124 @@ "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": { @@ -676,6 +794,45 @@ "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": { diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index c1e909e38d75b..4b62e8e12ee30 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -140,6 +140,29 @@ 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 }], @@ -152,6 +175,13 @@ router.addRoute('route-http2-retry', { }), }); +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 }], diff --git a/packages/@aws-cdk/aws-appmesh/test/test.route.ts b/packages/@aws-cdk/aws-appmesh/test/test.route.ts index 43c2d942a669b..b3c1ae674a6f4 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.route.ts @@ -576,6 +576,278 @@ export = { }, }, + '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) { const app = new cdk.App(); // GIVEN From d3f428435976c55ca950279cfc841665fd504370 Mon Sep 17 00:00:00 2001 From: Alex Johnson Date: Thu, 11 Mar 2021 16:53:55 -0800 Subject: [PATCH 14/20] fix(appmesh): Move Client Policy from Virtual Service to backend structure (#12943) @sshver: > Client Policies are inherently not related to the Virtual Service. It should be thought of as the client (the VN) telling envoy what connections they want to allow to the server (the Virtual Service). The server shouldn't be the one to define what policies are used to enforce connections with itself. ## Description of changes I refactored the client policy from Virtual Service to a separate backend structure. This mirrors how our API is designed. Also ran `npm run lint -- --fix` and removed some comments to fix lint warnings. ```ts /* Old backend defaults */ backendsDefaultClientPolicy: appmesh.ClientPolicy.fileTrust({ certificateChain: 'path-to-certificate', }), /* result of this PR */ backendDefaults: { clientPolicy: appmesh.ClientPolicy.fileTrust({ certificateChain: 'path-to-certificate', }), }, ``` ```ts /* Old Virtual Service with client policy */ const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), clientPolicy: appmesh.ClientPolicy.fileTrust({ certificateChain: 'path-to-certificate', ports: [8080, 8081], }), }); /* result of this PR; client policy is defined in the Virtual Node */ const service1 = new appmesh.VirtualService(stack, 'service-1', { virtualServiceName: 'service1.domain.local', virtualServiceProvider: appmesh.VirtualServiceProvider.none(mesh), }); const node = new appmesh.VirtualNode(stack, 'test-node', { mesh, serviceDiscovery: appmesh.ServiceDiscovery.dns('test'), }); node.addBackend({ virtualService: service1, clientPolicy: appmesh.ClientPolicy.fileTrust({ certificateChain: 'path-to-certificate', ports: [8080, 8081], }), }); ``` BREAKING CHANGE: 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` Resolves #11996 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/extensions/appmesh.ts | 2 +- packages/@aws-cdk/aws-appmesh/README.md | 24 +++--- .../aws-appmesh/lib/shared-interfaces.ts | 79 +++++++++++++++++++ .../aws-appmesh/lib/virtual-gateway.ts | 11 ++- .../@aws-cdk/aws-appmesh/lib/virtual-node.ts | 23 +++--- .../aws-appmesh/lib/virtual-service.ts | 24 ------ .../@aws-cdk/aws-appmesh/test/integ.mesh.ts | 35 ++++---- .../aws-appmesh/test/test.health-check.ts | 2 - .../@aws-cdk/aws-appmesh/test/test.mesh.ts | 4 +- .../aws-appmesh/test/test.virtual-gateway.ts | 8 +- .../aws-appmesh/test/test.virtual-node.ts | 21 ++--- .../aws-appmesh/test/test.virtual-router.ts | 18 ++--- 12 files changed, 153 insertions(+), 98 deletions(-) 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 7749683fb4235..95220dc1ea3b4 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 @@ -347,7 +347,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/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 63203d6b365d1..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 @@ -437,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', }); @@ -464,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/shared-interfaces.ts b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts index 831db66e49e0c..007f67c4a7a9b 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts @@ -1,5 +1,7 @@ import * as cdk from '@aws-cdk/core'; import { CfnVirtualGateway, CfnVirtualNode } from './appmesh.generated'; +import { ClientPolicy } from './client-policy'; +import { IVirtualService } from './virtual-service'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order @@ -194,3 +196,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 1e1144fed1038..d2f0a873a0849 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.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index 4b62e8e12ee30..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'), }); 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.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', { From 278029f25b41d956091835364e5a8de91429712c Mon Sep 17 00:00:00 2001 From: Benura Abeywardena <43112139+BLasan@users.noreply.github.com> Date: Fri, 12 Mar 2021 15:34:56 +0530 Subject: [PATCH 15/20] fix(cloudwatch): cannot create Alarms from labeled metrics that start with a digit (#13560) fixes #13434 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts | 3 +-- .../aws-cloudwatch/test/integ.alarm-with-label.expected.json | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) 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": { From 4769b313570e7d0f4afb05f216fc17faaa1ed2c7 Mon Sep 17 00:00:00 2001 From: Benura Abeywardena <43112139+BLasan@users.noreply.github.com> Date: Fri, 12 Mar 2021 16:14:44 +0530 Subject: [PATCH 16/20] chore(aws-cdk-readme): replace deprecated method used in aws-chatbot README.md (#13521) Currently addLambdaInvokeCommandPermissions method used to get the permissions, which is a deprecated method now. Use addToPolicy method to get necessary permissions fix: #13444 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-chatbot/README.md | 5 ----- 1 file changed, 5 deletions(-) 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: [ From 46114bb1f4702019a8873b9162d0a9f10763bc61 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 12 Mar 2021 12:27:49 +0100 Subject: [PATCH 17/20] fix(autoscaling): AutoScaling on percentile metrics doesn't work (#13366) AutoScaling on percentile metrics did not work because the `MetricAggregationType` was trying to be derived from the metric, and it can only be MIN, MAX or AVG. Figure out what the metric aggregation type does, default it to AVERAGE if no other suitable value can be determined, and also make it and the evaluation periods configurable while we're at it. Fixes #13144. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/step-scaling-policy.ts | 29 +++++-- .../test/test.step-scaling-policy.ts | 77 ++++++++++++++++++- .../lib/step-scaling-policy.ts | 29 +++++-- .../aws-autoscaling/test/scaling.test.ts | 67 +++++++++++++++- 4 files changed, 190 insertions(+), 12 deletions(-) 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 417ecf34f1970..8b9f7b2644267 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-policy.ts @@ -51,6 +51,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 { @@ -92,7 +111,7 @@ export class StepScalingPolicy extends CoreConstruct { 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, }); @@ -110,7 +129,7 @@ export class StepScalingPolicy extends CoreConstruct { 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)); @@ -122,7 +141,7 @@ export class StepScalingPolicy extends CoreConstruct { 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, }); @@ -140,7 +159,7 @@ export class StepScalingPolicy extends CoreConstruct { 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)); @@ -197,7 +216,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-autoscaling/lib/step-scaling-policy.ts b/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts index c3b51a892c222..307eaf525ae55 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/step-scaling-policy.ts @@ -52,6 +52,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 { @@ -93,7 +112,7 @@ export class StepScalingPolicy extends CoreConstruct { 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, }); @@ -111,7 +130,7 @@ export class StepScalingPolicy extends CoreConstruct { 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)); @@ -123,7 +142,7 @@ export class StepScalingPolicy extends CoreConstruct { 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, }); @@ -141,7 +160,7 @@ export class StepScalingPolicy extends CoreConstruct { 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)); @@ -161,7 +180,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 5207897cd1786..5c1f8947b09bd 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'; @@ -277,6 +277,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; From 81cf548b115e0e65d8dedf54d3efabdcbfda536b Mon Sep 17 00:00:00 2001 From: Robert Djurasaj Date: Fri, 12 Mar 2021 09:14:35 -0700 Subject: [PATCH 18/20] chore(docs): fix typos across the board (#13435) Fix bunch of docstring, docs and param typos. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-apigateway/lib/cors.ts | 2 +- .../@aws-cdk/aws-apigateway/lib/gateway-response.ts | 2 +- packages/@aws-cdk/aws-apigateway/lib/resource.ts | 6 +++--- packages/@aws-cdk/aws-apigateway/lib/restapi.ts | 4 ++-- packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts | 2 +- packages/@aws-cdk/aws-apigatewayv2/README.md | 2 +- packages/@aws-cdk/aws-batch/README.md | 4 ++-- packages/@aws-cdk/aws-batch/lib/exposed-secret.ts | 12 ++++++------ .../aws-batch/lib/job-definition-image-config.ts | 2 +- packages/@aws-cdk/aws-certificatemanager/README.md | 2 +- .../aws-certificatemanager/lib/certificate.ts | 8 ++++---- .../lib/dns-validated-certificate.ts | 2 +- packages/@aws-cdk/aws-dynamodb/README.md | 4 ++-- packages/@aws-cdk/aws-dynamodb/lib/table.ts | 4 ++-- packages/@aws-cdk/aws-ec2/lib/launch-template.ts | 2 +- packages/@aws-cdk/aws-ec2/lib/network-util.ts | 4 ++-- packages/@aws-cdk/aws-ec2/lib/vpc.ts | 2 +- packages/@aws-cdk/aws-lambda/lib/alias.ts | 2 +- packages/@aws-cdk/aws-lambda/lib/code.ts | 2 +- packages/@aws-cdk/aws-lambda/lib/function.ts | 4 ++-- packages/@aws-cdk/aws-lambda/lib/lambda-version.ts | 2 +- packages/@aws-cdk/aws-route53/README.md | 2 +- .../lib/vpc-endpoint-service-domain-name.ts | 6 +++--- packages/@aws-cdk/aws-s3-assets/README.md | 2 +- packages/@aws-cdk/aws-s3/lib/bucket.ts | 6 +++--- packages/@aws-cdk/aws-stepfunctions/README.md | 2 +- .../aws-stepfunctions/lib/step-functions-task.ts | 4 ++-- packages/@aws-cdk/pipelines/README.md | 6 +++--- 28 files changed, 51 insertions(+), 51 deletions(-) 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 32c400c52d23e..51a6ae53a7040 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-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 191024a197a11..ba3c0c1740597 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 @@ -37,7 +37,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 f1a5cd442e9d1..98637e43409ed 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. * 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 9334192f5610d..84156abcec214 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -1555,7 +1555,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; @@ -1613,7 +1613,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-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/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 2a4e524fc5150..8d878dde26024 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -1597,7 +1597,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-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 fec1e1821270e..b78859bf3515c 100644 --- a/packages/@aws-cdk/aws-lambda/lib/code.ts +++ b/packages/@aws-cdk/aws-lambda/lib/code.ts @@ -60,7 +60,7 @@ 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 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-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 2af889661098a..9098503b625f4 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 @@ -138,7 +138,7 @@ export class VpcEndpointServiceDomainName extends CoreConstruct { // 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: { @@ -147,8 +147,8 @@ export class VpcEndpointServiceDomainName extends CoreConstruct { 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/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 630aa9f02bfcc..c4c472a01ca94 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-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/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 From 64da84be5c60bb8132551bcc27a7ca9c7effe95d Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Fri, 12 Mar 2021 17:59:42 +0100 Subject: [PATCH 19/20] fix(region-info): ap-northeast-3 data not correctly registered (#13564) The region information for ap-northeast-3 was not correctly registered as the region was missing from the `AWS_REGIONS` list in the `aws-entities.ts` file. This addresses the gap, and adds a validation at the beginning of `generate-static-data.ts` to ensure no "new" region is introduced here without also being introduced in the `AWS_REGIONS` list. Fixes #13561 Credits to @robertd who had a draft PR with similar changes, which I only saw once it was too late. I've retro-fitted all the good ideas they had which I did not have on first intention - so thank you @robertd. Co-Authored-By: @robertd ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../region-info/build-tools/aws-entities.ts | 53 +++++------ .../region-info/build-tools/fact-tables.ts | 93 +++++++++---------- .../build-tools/generate-static-data.ts | 18 ++++ .../__snapshots__/region-info.test.js.snap | 20 ++++ 4 files changed, 110 insertions(+), 74 deletions(-) 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..2f47e02f64da4 100644 --- a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts +++ b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts @@ -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..ab12430e57c84 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 @@ -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", From 165a3d877b7ab23f29e42e1e74ee7c5cb35b7f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christophe=20Boug=C3=A8re?= Date: Fri, 12 Mar 2021 18:39:53 +0100 Subject: [PATCH 20/20] feat(aws-elasticloadbalancingv2): add protocol version for ALB TargetGroups (#13570) Fixes #12869 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-elasticloadbalancingv2/README.md | 14 +++++++++++++ .../lib/alb/application-target-group.ts | 11 +++++++++- .../lib/shared/enums.ts | 20 +++++++++++++++++++ .../test/alb/target-group.test.ts | 18 +++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) 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 74938a08ee745..e7a6b2eef27d1 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'; @@ -28,6 +28,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. * @@ -110,8 +117,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();