From ec6e66081b810f3342967ca2dd94c8ecf3d8bcd8 Mon Sep 17 00:00:00 2001 From: Darren Date: Fri, 12 Mar 2021 19:21:11 -0600 Subject: [PATCH 01/32] WIP: topic rule test --- packages/@aws-cdk/aws-iot/README.md | 8 ++ .../@aws-cdk/aws-iot/lib/topic-rule-action.ts | 104 ++++++++++++++++ packages/@aws-cdk/aws-iot/lib/topic-rule.ts | 111 ++++++++++++++++++ packages/@aws-cdk/aws-iot/package.json | 12 +- .../@aws-cdk/aws-iot/test/topic-rule.test.ts | 20 ++++ 5 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk/aws-iot/lib/topic-rule-action.ts create mode 100644 packages/@aws-cdk/aws-iot/lib/topic-rule.ts create mode 100644 packages/@aws-cdk/aws-iot/test/topic-rule.test.ts diff --git a/packages/@aws-cdk/aws-iot/README.md b/packages/@aws-cdk/aws-iot/README.md index 7334c9d4e108e..e21793b631929 100644 --- a/packages/@aws-cdk/aws-iot/README.md +++ b/packages/@aws-cdk/aws-iot/README.md @@ -9,6 +9,14 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- diff --git a/packages/@aws-cdk/aws-iot/lib/topic-rule-action.ts b/packages/@aws-cdk/aws-iot/lib/topic-rule-action.ts new file mode 100644 index 0000000000000..f05ef04dc1972 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/topic-rule-action.ts @@ -0,0 +1,104 @@ +import { CfnTopicRule } from './iot.generated'; +import { ITopicRule } from './topic-rule'; + +/** + * An abstract action for a topic rule. + */ +export interface ITopicRuleAction { + /** + * Returns the topic rule action specification + */ + bind(topicRule: ITopicRule): TopicRuleActionConfig; +} + +/** + * Properties for a topic rule action. + */ +export interface TopicRuleActionConfig { + /** + * Describes an action that updates a CloudWatch alarm. + */ + readonly cloudwatchAlarm?: CfnTopicRule.CloudwatchAlarmActionProperty; + /** + * Describes an action that captures a CloudWatch metric. + */ + readonly cloudwatchAlarmMetric?: CfnTopicRule.CloudwatchMetricActionProperty; + /** + * Describes an action to write to a DynamoDB table. + * + * The tableName, hashKeyField, and rangeKeyField values must match the values + * used when you created the table. + * + * The hashKeyValue and rangeKeyvalue fields use a substitution template + * syntax. + * These templates provide data at runtime. The syntax is as follows: + * ${sql-expression}. + * + * You can specify any valid expression in a WHERE or SELECT clause, including JSON + * properties, comparisons, calculations, and functions. For example, the following + * field uses the third level of the topic: + * + * "hashKeyValue": "${topic(3)}" + * + * The following field uses the timestamp: + * + * "rangeKeyValue": "${timestamp()}" + */ + readonly dynamoDB?: CfnTopicRule.DynamoDBActionProperty; + /** + * Describes an action to write to a DynamoDB table. + * + * This DynamoDB action writes each attribute in the message payload into it's + * own column in the DynamoDB table. + */ + readonly dynamoDBv2?: CfnTopicRule.DynamoDBv2ActionProperty; + /** + * Describes an action that writes data to an Amazon Elasticsearch Service domain. + */ + readonly elasticseach?: CfnTopicRule.ElasticsearchActionProperty; + /** + * Describes an action that writes data to an Amazon Kinesis Firehose stream. + */ + readonly firehose?: CfnTopicRule.FirehoseActionProperty; + /** + * Send data to an HTTPS endpoint. + */ + readonly http?: CfnTopicRule.HttpActionProperty; + /** + * Sends message data to an AWS IoT Analytics channel. + */ + readonly iotAnalytics?: CfnTopicRule.IotAnalyticsActionProperty; + /** + * Describes an action to send data from an MQTT message that triggered the + * rule to AWS IoT SiteWise asset properties. + */ + readonly iotSiteWise?: CfnTopicRule.IotSiteWiseActionProperty; + /** + * Describes an action to write data to an Amazon Kinesis stream. + */ + readonly kinesis?: CfnTopicRule.KinesisActionProperty; + /** + * Describes an action to invoke a Lambda function. + */ + readonly lambdaA?: CfnTopicRule.LambdaActionProperty; + /** + * Describes an action to republish to another topic. + */ + readonly republish?: CfnTopicRule.RepublishActionProperty; + /** + * Describes an action to write data to an Amazon S3 bucket. + */ + readonly s3?: CfnTopicRule.S3ActionProperty; + /** + * Describes an action to publish to an Amazon SNS topic. + */ + readonly sns?: CfnTopicRule.SnsActionProperty; + /** + * Describes an action to publish data to an Amazon SQS queue. + */ + readonly sqs?: CfnTopicRule.SqsActionProperty; + /** + * Starts execution of a Step Functions state machine. + */ + readonly stepFunctions?: CfnTopicRule.StepFunctionsActionProperty; +} diff --git a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts new file mode 100644 index 0000000000000..c719413eb7ac0 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts @@ -0,0 +1,111 @@ +import { Resource, IResource, Lazy } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnTopicRule } from './iot.generated'; +import { ITopicRuleAction } from './topic-rule-action'; +import { parseTopicRuleArn } from './util'; + +/** + * An IoT Topic Rule + */ +export interface ITopicRule extends IResource { + /** + * Then name of the topic rule + */ + readonly ruleName: string; + /** + * Then name of the topic rule + */ + readonly topicRuleArn: string; +} +/** + * Properties for defining an IoT Topic Rule. + */ +export interface TopicRuleProps { + /** + * The name of the rule. + * + * @default - generated + */ + readonly ruleName?: string + /** + * The rule actions. + * + * @default - empty + */ + readonly actions?: ITopicRuleAction[]; + /** + * The rule enabled status. + * + * @default - false + */ + readonly ruleDisabled?: boolean; + /** + * The rule sql. + */ + readonly sql: string; + /** + * The rule AWS IoT SQL version. + * + * @default - 2012-17-10 + */ + readonly awsIotSqlVersion?: string; + /** + * The topic rule description. + */ + readonly description?: string; + /** + * The rule actions to preform on error. + */ + readonly errorAction?: ITopicRuleAction; +} + +/** + * A new topic rule + */ +export class TopicRule extends Resource implements ITopicRule { + public static fromReceiptRuleName(scope: Construct, id: string, topicRuleName: string): ITopicRule { + class Import extends Resource implements ITopicRule { + public readonly ruleName = topicRuleName; + public readonly topicRuleArn = parseTopicRuleArn(scope, topicRuleName); + } + return new Import(scope, id); + } + + public readonly ruleName: string; + public readonly topicRuleArn: string; + private readonly actions = new Array(); + + constructor(scope: Construct, id: string, props: TopicRuleProps) { + super(scope, id, { + physicalName: props.ruleName || 'TODO', + }); + + const resource = new CfnTopicRule(this, 'Resource', { + topicRulePayload: { + sql: props.sql, + ruleDisabled: props.ruleDisabled || false, + actions: Lazy.any({ produce: () => this.renderActions() }), + }, + }); + + this.ruleName = resource.ref; + this.topicRuleArn = resource.attrArn; + for (const action of props.actions || []) { + this.addAction(action); + } + } + + /** + * Adds an action to this topic rule. + */ + public addAction(action: ITopicRuleAction) { + this.actions.push(action.bind(this)); + } + + public renderActions() { + if (this.actions.length === 0) { + return undefined; + } + return this.actions; + } +} diff --git a/packages/@aws-cdk/aws-iot/package.json b/packages/@aws-cdk/aws-iot/package.json index baec64baf8142..e3b4adf085028 100644 --- a/packages/@aws-cdk/aws-iot/package.json +++ b/packages/@aws-cdk/aws-iot/package.json @@ -74,7 +74,8 @@ "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "pkglint": "0.0.0" + "pkglint": "0.0.0", + "nodeunit-shim": "0.0.0" }, "dependencies": { "@aws-cdk/core": "0.0.0", @@ -88,8 +89,15 @@ "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, + "awslint": { + "exclude": [ + "docs-public-apis:@aws-cdk/aws-iot.TopicRule.fromReceiptRuleName", + "docs-public-apis:@aws-cdk/aws-iot.TopicRule.renderActions", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionProperty.cloudwatchAlarm" + ] + }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts new file mode 100644 index 0000000000000..f1dda8417a379 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts @@ -0,0 +1,20 @@ +import { expect } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/core'; +import { nodeunitShim, Test } from 'nodeunit-shim'; +import { TopicRule } from '../lib'; + +nodeunitShim({ + 'can create topic rules'(test: Test) { + // GIVEN + const stack = new Stack(); + // WHEN + new TopicRule(stack, 'IotTopicRule', { + sql: 'SELECT * FROM \'topic/subtopc\'', + }); + + // THEN + expect(stack).toMatch({}); + test.done(); + }, +}); + From cabba5089610a0f7e56f4c035094286587647759 Mon Sep 17 00:00:00 2001 From: Darren Date: Sat, 13 Mar 2021 17:59:38 -0600 Subject: [PATCH 02/32] feat(aws-iot): topic rules and actions --- .../@aws-cdk/aws-iot-actions/.eslintrc.js | 3 + packages/@aws-cdk/aws-iot-actions/.gitignore | 19 ++ packages/@aws-cdk/aws-iot-actions/.npmignore | 27 +++ packages/@aws-cdk/aws-iot-actions/LICENSE | 201 ++++++++++++++++++ packages/@aws-cdk/aws-iot-actions/NOTICE | 2 + packages/@aws-cdk/aws-iot-actions/README.md | 30 +++ .../@aws-cdk/aws-iot-actions/jest.config.js | 2 + .../@aws-cdk/aws-iot-actions/lib/index.ts | 3 + .../@aws-cdk/aws-iot-actions/lib/lambda.ts | 49 +++++ .../@aws-cdk/aws-iot-actions/lib/republish.ts | 59 +++++ packages/@aws-cdk/aws-iot-actions/lib/sns.ts | 71 +++++++ packages/@aws-cdk/aws-iot-actions/lib/util.ts | 27 +++ .../@aws-cdk/aws-iot-actions/package.json | 112 ++++++++++ .../aws-iot-actions/test/iot-actions.test.ts | 104 +++++++++ packages/@aws-cdk/aws-iot/lib/index.ts | 2 + .../@aws-cdk/aws-iot/lib/topic-rule-action.ts | 4 +- packages/@aws-cdk/aws-iot/lib/topic-rule.ts | 41 +++- packages/@aws-cdk/aws-iot/lib/util.ts | 5 + packages/@aws-cdk/aws-iot/package.json | 22 +- .../@aws-cdk/aws-iot/test/topic-rule.test.ts | 68 +++++- packages/@aws-cdk/aws-iot/test/util.test.ts | 12 ++ 21 files changed, 852 insertions(+), 11 deletions(-) create mode 100644 packages/@aws-cdk/aws-iot-actions/.eslintrc.js create mode 100644 packages/@aws-cdk/aws-iot-actions/.gitignore create mode 100644 packages/@aws-cdk/aws-iot-actions/.npmignore create mode 100644 packages/@aws-cdk/aws-iot-actions/LICENSE create mode 100644 packages/@aws-cdk/aws-iot-actions/NOTICE create mode 100644 packages/@aws-cdk/aws-iot-actions/README.md create mode 100644 packages/@aws-cdk/aws-iot-actions/jest.config.js create mode 100644 packages/@aws-cdk/aws-iot-actions/lib/index.ts create mode 100644 packages/@aws-cdk/aws-iot-actions/lib/lambda.ts create mode 100644 packages/@aws-cdk/aws-iot-actions/lib/republish.ts create mode 100644 packages/@aws-cdk/aws-iot-actions/lib/sns.ts create mode 100644 packages/@aws-cdk/aws-iot-actions/lib/util.ts create mode 100644 packages/@aws-cdk/aws-iot-actions/package.json create mode 100644 packages/@aws-cdk/aws-iot-actions/test/iot-actions.test.ts create mode 100644 packages/@aws-cdk/aws-iot/lib/util.ts create mode 100644 packages/@aws-cdk/aws-iot/test/util.test.ts diff --git a/packages/@aws-cdk/aws-iot-actions/.eslintrc.js b/packages/@aws-cdk/aws-iot-actions/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/.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-iot-actions/.gitignore b/packages/@aws-cdk/aws-iot-actions/.gitignore new file mode 100644 index 0000000000000..d8a8561d50885 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/.gitignore @@ -0,0 +1,19 @@ +*.js +*.js.map +*.d.ts +tsconfig.json +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk +!.eslintrc.js +!jest.config.js + +junit.xml \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/.npmignore b/packages/@aws-cdk/aws-iot-actions/.npmignore new file mode 100644 index 0000000000000..63ab95621c764 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/.npmignore @@ -0,0 +1,27 @@ +# 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/ \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/LICENSE b/packages/@aws-cdk/aws-iot-actions/LICENSE new file mode 100644 index 0000000000000..28e4bdcec77ec --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-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-iot-actions/NOTICE b/packages/@aws-cdk/aws-iot-actions/NOTICE new file mode 100644 index 0000000000000..5fc3826926b5b --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/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-iot-actions/README.md b/packages/@aws-cdk/aws-iot-actions/README.md new file mode 100644 index 0000000000000..e0bd7aad9352f --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/README.md @@ -0,0 +1,30 @@ +# AWS IoT 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 + +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + +--- + + + +This module contains integration classes to add actions to IoT Topic Rules. +Instances of these classes should be passed to the `rule.addAction()` method. + +Currently supported are: + [Lambda](https://docs.aws.amazon.com/iot/latest/developerguide/lambda-rule-action.html) + +See the README of `@aws-cdk/aws-iot` for more information. diff --git a/packages/@aws-cdk/aws-iot-actions/jest.config.js b/packages/@aws-cdk/aws-iot-actions/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-iot-actions/lib/index.ts b/packages/@aws-cdk/aws-iot-actions/lib/index.ts new file mode 100644 index 0000000000000..5621e67ba8700 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/index.ts @@ -0,0 +1,3 @@ +export * from './lambda'; +export * from './republish'; +export * from './sns'; diff --git a/packages/@aws-cdk/aws-iot-actions/lib/lambda.ts b/packages/@aws-cdk/aws-iot-actions/lib/lambda.ts new file mode 100644 index 0000000000000..2de0229e54306 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/lambda.ts @@ -0,0 +1,49 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; + +/** + * Construction properties for a Lambda action. + */ +export interface LambdaProps { + /** + * The Lambda function to invoke + */ + readonly function: lambda.IFunction; +} + +/** + * Calls an AWS Lambda function + */ +export class Lambda implements iot.ITopicRuleAction { + constructor(private readonly props: LambdaProps) { + } + + public bind(rule: iot.ITopicRule): iot.TopicRuleActionConfig { + // Allow rule to invoke lambda function + const permissionId = 'AllowIot'; + if (!this.props.function.permissionsNode.tryFindChild(permissionId)) { + this.props.function.addPermission(permissionId, { + action: 'lambda:InvokeFunction', + principal: new iam.ServicePrincipal('iot.amazonaws.com'), + sourceAccount: cdk.Aws.ACCOUNT_ID, + }); + } + + // Ensure permission is deployed before rule + const permission = this.props.function.permissionsNode.tryFindChild(permissionId) as lambda.CfnPermission; + if (permission) { + rule.node.addDependency(permission); + } else { + // tsling:disable-next-line:max-line-length + rule.node.addWarning('This rule is using a Lambda action with an imported function. Ensure permssion is given to IOT to invoke that function.'); + } + + return { + lambda: { + functionArn: this.props.function.functionArn, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot-actions/lib/republish.ts b/packages/@aws-cdk/aws-iot-actions/lib/republish.ts new file mode 100644 index 0000000000000..a5228eab9efc8 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/republish.ts @@ -0,0 +1,59 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import { singletonTopicRuleRole } from './util'; + +/** + * Construction properties for a Republish action. + */ +export interface RepublishProps { + /** + * The Quality of Service (Qos) level to use when republishing messages. + * + * @default - 0 + */ + readonly qos?: number; + /** + * + * The MQTT topic to which to republish the message. + * + * To republish to a reserved topic, which begins with `$`, use `$$` instead. + * + * For example, to republish to the device shadow topic + * `$aws/things/MyThing/shadow/update`, specify the topic as + * `$$aws/things/MyThing/shadow/update`. + */ + readonly topic: string; + /** + * The IAM role that grants access. + * + * @default - a role will be created + */ + readonly role?: iam.IRole; +} + +/** + * Publishes to a IoT Topic + */ +export class Republish implements iot.ITopicRuleAction { + constructor(private readonly props: RepublishProps) { + } + + public bind(rule: iot.ITopicRule): iot.TopicRuleActionConfig { + // Allow rule to publish to topic + const role = this.props.role || singletonTopicRuleRole(rule, [new iam.PolicyStatement({ + actions: ['iot:Publish'], + resources: [this.props.topic], + })]); + + // Ensure permission is deployed before rule + rule.node.addDependency(role); + + return { + republish: { + qos: this.props.qos || 0, + topic: this.props.topic, + roleArn: role.roleArn, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot-actions/lib/sns.ts b/packages/@aws-cdk/aws-iot-actions/lib/sns.ts new file mode 100644 index 0000000000000..7af980a8f5344 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/sns.ts @@ -0,0 +1,71 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as sns from '@aws-cdk/aws-sns'; +import { singletonTopicRuleRole } from './util'; + +/** + * The allowd message formats + */ +export enum MessageFormats { + /** + * JSON topic message format + */ + JSON = 'JSON', + /** + * RAW topic message format + */ + RAW = 'RAW', +} +/** + * Construction properties for a sns publish action. + */ +export interface SnsProps { + /** + * The Topic to publish on + */ + readonly topic: sns.ITopic; + /** + * (Optional) The message format of the message to publish. Accepted values + * are "JSON" and "RAW". The default value of the attribute is "RAW". SNS uses + * this setting to determine if the payload should be parsed and relevant + * platform-specific bits of the payload should be extracted. + * + * https://docs.aws.amazon.com/sns/latest/dg/json-formats.html + * + * @default - MessageFormats.RAW + */ + readonly messageFormat?: MessageFormats; + /** + * The IAM role that grants access. + * + * @default - a role will be created + */ + readonly role?: iam.IRole; +} + +/** + * Publishes to a Topic + */ +export class Sns implements iot.ITopicRuleAction { + constructor(private readonly props: SnsProps) { + } + + public bind(rule: iot.ITopicRule): iot.TopicRuleActionConfig { + // Allow rule to publish to topic + const role = this.props.role || singletonTopicRuleRole(rule, [new iam.PolicyStatement({ + actions: ['sns:Publish'], + resources: [this.props.topic.topicArn], + })]); + + // Ensure permission is deployed before rule + rule.node.addDependency(role); + + return { + sns: { + messageFormat: this.props.messageFormat || MessageFormats.RAW, + targetArn: this.props.topic.topicArn, + roleArn: role.roleArn, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot-actions/lib/util.ts b/packages/@aws-cdk/aws-iot-actions/lib/util.ts new file mode 100644 index 0000000000000..876bb0e6702f0 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/util.ts @@ -0,0 +1,27 @@ +import * as iam from '@aws-cdk/aws-iam'; +import { IConstruct, Stack } from '@aws-cdk/core'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +/** + * Obtain the Role for the Topic Rule + * + * If a role already exists, it will be returned. This ensures that if multiple + * events have the same target, they will share a role. + */ +export function singletonTopicRuleRole(scope: IConstruct, policyStatements: iam.PolicyStatement[]): iam.IRole { + const stack = Stack.of(scope); + const id = 'DefaultPolicy'; + const existing = stack.node.tryFindChild(id) as iam.IRole; + if (existing) { return existing; } + + const role = new iam.Role(scope as Construct, id, { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + + policyStatements.forEach(role.addToPolicy.bind(role)); + + return role; +} diff --git a/packages/@aws-cdk/aws-iot-actions/package.json b/packages/@aws-cdk/aws-iot-actions/package.json new file mode 100644 index 0000000000000..f1b2515dbbd09 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/package.json @@ -0,0 +1,112 @@ +{ + "name": "@aws-cdk/aws-iot-actions", + "version": "0.0.0", + "description": "Topic rule actions for AWS IoT", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "java": { + "package": "software.amazon.awscdk.services.iot.actions", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "iot-actions", + "versionSuffix": ".DEVPREVIEW" + } + }, + "dotnet": { + "namespace": "Amazon.CDK.AWS.IoT.Actions", + "packageId": "Amazon.CDK.AWS.IoT.Actions", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "python": { + "distName": "aws-cdk.aws-iot-actions", + "module": "aws_cdk.aws_iot_actions", + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ] + } + }, + "projectReferences": true + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-iot-actions" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "cdk-integ", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "cfn2ts": "cfn2ts", + "build+test+package": "yarn build+test && yarn package", + "build+test": "yarn build && yarn test", + "compat": "cdk-compat", + "gen": "cfn2ts", + "rosetta:extract": "yarn --silent jsii-rosetta extract" + }, + "cdk-build": { + "cloudformation": "AWS::IoT", + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": true + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "iot", + "actions" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", + "cfn2ts": "0.0.0", + "pkglint": "0.0.0", + "nodeunit-shim": "0.0.0" + }, + "dependencies": { + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0" + }, + "homepage": "https://github.com/aws/aws-cdk", + "peerDependencies": { + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "awslint": { + "exclude": [] + }, + "maturity": "experimental", + "stability": "experimental", + "awscdkio": { + "announce": false + }, + "publishConfig": { + "tag": "latest" + } +} diff --git a/packages/@aws-cdk/aws-iot-actions/test/iot-actions.test.ts b/packages/@aws-cdk/aws-iot-actions/test/iot-actions.test.ts new file mode 100644 index 0000000000000..9a72968a04358 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/iot-actions.test.ts @@ -0,0 +1,104 @@ +import '@aws-cdk/assert/jest'; +import * as iot from '@aws-cdk/aws-iot'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sns from '@aws-cdk/aws-sns'; +import { Stack } from '@aws-cdk/core'; +import * as actions from '../lib'; + +let stack: Stack; +let rule: iot.TopicRule; + +test('add lambda action', () => { + stack = new Stack(); + rule = new iot.TopicRule(stack, 'TopicRule', { + sql: 'SELECT * FROM \'topic/subtopic\'', + }); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('boom'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_8_10, + }); + + rule.addAction(new actions.Lambda({ + function: fn, + })); + + expect(stack).toHaveResourceLike('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Lambda: { FunctionArn: { 'Fn::GetAtt': ['Function76856677', 'Arn'] } }, + }, + ], + RuleDisabled: false, + Sql: 'SELECT * FROM \'topic/subtopic\'', + }, + }); + + expect(stack).toHaveResourceLike('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn', + ], + }, + Principal: 'iot.amazonaws.com', + SourceAccount: { + Ref: 'AWS::AccountId', + }, + }); +}); +test('add sns action', () => { + stack = new Stack(); + rule = new iot.TopicRule(stack, 'PublishSnsRule', { + sql: 'SELECT * FROM \'topic/subtopic\'', + }); + const topic = new sns.Topic(stack, 'MyTopic'); + + rule.addAction(new actions.Sns({ + topic: topic, + })); + + expect(stack).toHaveResourceLike('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Sns: { + MessageFormat: 'RAW', + RoleArn: { 'Fn::GetAtt': ['PublishSnsRuleDefaultPolicyF2EC542D', 'Arn'] }, + TargetArn: { Ref: 'MyTopic86869434' }, + }, + }, + ], + RuleDisabled: false, + Sql: 'SELECT * FROM \'topic/subtopic\'', + }, + }); +}); +test('add republish action', () => { + stack = new Stack(); + rule = new iot.TopicRule(stack, 'RepublishRule', { + sql: 'SELECT * FROM \'topic/subtopic\'', + }); + + rule.addAction(new actions.Republish({ + topic: '$$aws/things/MyThing/shadow/update', + })); + + expect(stack).toHaveResourceLike('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Republish: { + Qos: 0, + RoleArn: { 'Fn::GetAtt': ['RepublishRuleDefaultPolicy143E0758', 'Arn'] }, + Topic: '$$aws/things/MyThing/shadow/update', + }, + }, + ], + RuleDisabled: false, + Sql: 'SELECT * FROM \'topic/subtopic\'', + }, + }); +}); diff --git a/packages/@aws-cdk/aws-iot/lib/index.ts b/packages/@aws-cdk/aws-iot/lib/index.ts index 4f78a6cf531e3..11c0c38a7db4a 100644 --- a/packages/@aws-cdk/aws-iot/lib/index.ts +++ b/packages/@aws-cdk/aws-iot/lib/index.ts @@ -1,2 +1,4 @@ +export * from './topic-rule'; +export * from './topic-rule-action'; // AWS::IoT CloudFormation Resources: export * from './iot.generated'; diff --git a/packages/@aws-cdk/aws-iot/lib/topic-rule-action.ts b/packages/@aws-cdk/aws-iot/lib/topic-rule-action.ts index f05ef04dc1972..278b2536131f9 100644 --- a/packages/@aws-cdk/aws-iot/lib/topic-rule-action.ts +++ b/packages/@aws-cdk/aws-iot/lib/topic-rule-action.ts @@ -55,7 +55,7 @@ export interface TopicRuleActionConfig { /** * Describes an action that writes data to an Amazon Elasticsearch Service domain. */ - readonly elasticseach?: CfnTopicRule.ElasticsearchActionProperty; + readonly elasticsearch?: CfnTopicRule.ElasticsearchActionProperty; /** * Describes an action that writes data to an Amazon Kinesis Firehose stream. */ @@ -80,7 +80,7 @@ export interface TopicRuleActionConfig { /** * Describes an action to invoke a Lambda function. */ - readonly lambdaA?: CfnTopicRule.LambdaActionProperty; + readonly lambda?: CfnTopicRule.LambdaActionProperty; /** * Describes an action to republish to another topic. */ diff --git a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts index c719413eb7ac0..f8d90dab457aa 100644 --- a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts +++ b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts @@ -2,7 +2,7 @@ import { Resource, IResource, Lazy } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnTopicRule } from './iot.generated'; import { ITopicRuleAction } from './topic-rule-action'; -import { parseTopicRuleArn } from './util'; +import { parseRuleName } from './util'; /** * An IoT Topic Rule @@ -14,6 +14,19 @@ export interface ITopicRule extends IResource { readonly ruleName: string; /** * Then name of the topic rule + * + * @attribute + * + */ + readonly topicRuleArn: string; +} +/** + * A reference to an IoT Topic Rule. + */ +export interface TopicRuleAttributes { + /** + * The ARN of the IoT Topic Rule. At least one of bucketArn or bucketName must be + * defined in order to initialize a bucket ref. */ readonly topicRuleArn: string; } @@ -51,10 +64,14 @@ export interface TopicRuleProps { readonly awsIotSqlVersion?: string; /** * The topic rule description. + * + * @default - none */ readonly description?: string; /** * The rule actions to preform on error. + * + * @default - none */ readonly errorAction?: ITopicRuleAction; } @@ -63,11 +80,23 @@ export interface TopicRuleProps { * A new topic rule */ export class TopicRule extends Resource implements ITopicRule { - public static fromReceiptRuleName(scope: Construct, id: string, topicRuleName: string): ITopicRule { + /** + * Import topic rule attributes + */ + public static fromTopicRuleArn(scope: Construct, id: string, topicRuleArn: string): ITopicRule { + return TopicRule.fromTopicRuleAttributes(scope, id, { topicRuleArn }); + } + /** + * Import topic rule attributes + */ + public static fromTopicRuleAttributes(scope: Construct, id: string, attrs: TopicRuleAttributes): ITopicRule { + const topicRuleArn = attrs.topicRuleArn; + const ruleName = parseRuleName(attrs.topicRuleArn); class Import extends Resource implements ITopicRule { - public readonly ruleName = topicRuleName; - public readonly topicRuleArn = parseTopicRuleArn(scope, topicRuleName); + public readonly ruleName = ruleName; + public readonly topicRuleArn = topicRuleArn; } + return new Import(scope, id); } @@ -81,7 +110,11 @@ export class TopicRule extends Resource implements ITopicRule { }); const resource = new CfnTopicRule(this, 'Resource', { + ruleName: this.physicalName, topicRulePayload: { + ...props.errorAction, + description: props.description || '', + awsIotSqlVersion: props.awsIotSqlVersion || '2015-10-08', sql: props.sql, ruleDisabled: props.ruleDisabled || false, actions: Lazy.any({ produce: () => this.renderActions() }), diff --git a/packages/@aws-cdk/aws-iot/lib/util.ts b/packages/@aws-cdk/aws-iot/lib/util.ts new file mode 100644 index 0000000000000..b74eb8551c97a --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/util.ts @@ -0,0 +1,5 @@ +import { Fn } from '@aws-cdk/core'; + +export function parseRuleName(topicRuleArn: string): string { + return Fn.select(1, Fn.split('rule/', topicRuleArn)); +} diff --git a/packages/@aws-cdk/aws-iot/package.json b/packages/@aws-cdk/aws-iot/package.json index e3b4adf085028..e409ea5cdaf9a 100644 --- a/packages/@aws-cdk/aws-iot/package.json +++ b/packages/@aws-cdk/aws-iot/package.json @@ -93,7 +93,27 @@ "exclude": [ "docs-public-apis:@aws-cdk/aws-iot.TopicRule.fromReceiptRuleName", "docs-public-apis:@aws-cdk/aws-iot.TopicRule.renderActions", - "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionProperty.cloudwatchAlarm" + "no-unused-type:@aws-cdk/aws-iot.TopicRuleAttributes", + "props-physical-name:@aws-cdk/aws-iot.TopicRuleProps", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionProperty.cloudwatchAlarm", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleAttributes.ruleName", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleAttributes.topicRuleArn", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.cloudwatchAlarm", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.cloudwatchAlarmMetric", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.dynamoDB", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.dynamoDBv2", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.elasticsearch", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.firehose", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.http", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.iotAnalytics", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.iotSiteWise", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.kinesis", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.lambda", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.republish", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.s3", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.sns", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.sqs", + "props-default-doc:@aws-cdk/aws-iot.TopicRuleActionConfig.stepFunctions" ] }, "stability": "experimental", diff --git a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts index f1dda8417a379..a2b454a8b1e30 100644 --- a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts +++ b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts @@ -1,7 +1,7 @@ -import { expect } from '@aws-cdk/assert'; +import { expect, haveResource } from '@aws-cdk/assert'; import { Stack } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; -import { TopicRule } from '../lib'; +import { TopicRule, ITopicRuleAction, TopicRuleActionConfig } from '../lib'; nodeunitShim({ 'can create topic rules'(test: Test) { @@ -9,12 +9,72 @@ nodeunitShim({ const stack = new Stack(); // WHEN new TopicRule(stack, 'IotTopicRule', { - sql: 'SELECT * FROM \'topic/subtopc\'', + sql: 'SELECT * FROM \'topic/subtopic\'', + actions: [new DummyAction()], }); + // THEN + expect(stack).to(haveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [{ + Republish: { + RoleArn: 'arn:iam::::role/MyRole', + Topic: 'topic/subtopic', + }, + }], + AwsIotSqlVersion: '2015-10-08', + Description: '', + RuleDisabled: false, + Sql: 'SELECT * FROM \'topic/subtopic\'', + }, + RuleName: 'TODO', + })); + test.done(); + }, + 'can add role after construction'(test: Test) { + // GIVEN + const stack = new Stack(); + // WHEN + const rule = new TopicRule(stack, 'IotTopicRule', { + sql: 'SELECT * FROM \'topic/subtopic\'', + }); + + rule.addAction(new DummyAction()); // THEN - expect(stack).toMatch({}); + expect(stack).to(haveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [{ + Republish: { + RoleArn: 'arn:iam::::role/MyRole', + Topic: 'topic/subtopic', + }, + }], + AwsIotSqlVersion: '2015-10-08', + Description: '', + RuleDisabled: false, + Sql: 'SELECT * FROM \'topic/subtopic\'', + }, + RuleName: 'TODO', + })); + test.done(); + }, + 'from topic rule arn'(test: Test) { + const stack = new Stack(); + const topicRuleArn = 'arn:aws:iot:::rule/MyTopicRule'; + let topic = TopicRule.fromTopicRuleArn(stack, 'ImportedTopic', topicRuleArn ); + test.deepEqual(topic.ruleName, 'MyTopicRule'); test.done(); }, }); +class DummyAction implements ITopicRuleAction { + public bind(): TopicRuleActionConfig { + return { + republish: { + roleArn: 'arn:iam::::role/MyRole', + topic: 'topic/subtopic', + }, + }; + }; +}; + diff --git a/packages/@aws-cdk/aws-iot/test/util.test.ts b/packages/@aws-cdk/aws-iot/test/util.test.ts new file mode 100644 index 0000000000000..74f4e4b4e61a1 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/util.test.ts @@ -0,0 +1,12 @@ +import { nodeunitShim, Test } from 'nodeunit-shim'; +import { parseRuleName } from '../lib/util'; + +nodeunitShim({ + ruleNameFromArn: { + 'produce rule name from topic rule arn'(test: Test) { + const topicRuleArn = 'arn:aws:iot:::rule/hello'; + test.deepEqual(parseRuleName(topicRuleArn), 'hello'); + test.done(); + }, + }, +}); From 54654d17710c915c7593e80ae5101e53f55a6966 Mon Sep 17 00:00:00 2001 From: Darren Date: Sat, 13 Mar 2021 18:22:32 -0600 Subject: [PATCH 03/32] feat(aws-iot): topic rules and actions remove error action property --- packages/@aws-cdk/aws-iot/lib/topic-rule.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts index f8d90dab457aa..efdffb1888a3b 100644 --- a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts +++ b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts @@ -112,7 +112,6 @@ export class TopicRule extends Resource implements ITopicRule { const resource = new CfnTopicRule(this, 'Resource', { ruleName: this.physicalName, topicRulePayload: { - ...props.errorAction, description: props.description || '', awsIotSqlVersion: props.awsIotSqlVersion || '2015-10-08', sql: props.sql, From 0804eb441d284f016608534e0470eaf7a219d5c0 Mon Sep 17 00:00:00 2001 From: Darren Date: Sat, 13 Mar 2021 18:56:33 -0600 Subject: [PATCH 04/32] feat(aws-iot): topic rules and actions include `aws-iot-actions` --- packages/@aws-cdk/cloudformation-include/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index a177759248e07..d99681d9ff074 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -292,6 +292,7 @@ "@aws-cdk/aws-imagebuilder": "0.0.0", "@aws-cdk/aws-inspector": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-iot-actions": "0.0.0", "@aws-cdk/aws-iot1click": "0.0.0", "@aws-cdk/aws-iotanalytics": "0.0.0", "@aws-cdk/aws-iotevents": "0.0.0", From f44d96e80d3f063ac06722cb2c9cc81960b8d29c Mon Sep 17 00:00:00 2001 From: Darren Date: Sat, 13 Mar 2021 19:51:11 -0600 Subject: [PATCH 05/32] feat(aws-iot): topic rules and actions include as dependency --- packages/@aws-cdk/cloudformation-include/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index d99681d9ff074..79c3a4ad308de 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -143,6 +143,7 @@ "@aws-cdk/aws-imagebuilder": "0.0.0", "@aws-cdk/aws-inspector": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-iot-actions": "0.0.0", "@aws-cdk/aws-iot1click": "0.0.0", "@aws-cdk/aws-iotanalytics": "0.0.0", "@aws-cdk/aws-iotevents": "0.0.0", From a4d59da910e847323b9440d96ab9a3abd8c78989 Mon Sep 17 00:00:00 2001 From: Darren Date: Sun, 14 Mar 2021 12:04:45 -0500 Subject: [PATCH 06/32] feat(aws-iot): topic rules and actions add integration tests and remove circular dependency caused by ensure. The integrations show that two iam roles are generated. I'm not sure of the best course of action --- .../@aws-cdk/aws-iot-actions/lib/republish.ts | 13 ++- packages/@aws-cdk/aws-iot-actions/lib/sns.ts | 3 - packages/@aws-cdk/aws-iot-actions/lib/util.ts | 2 +- ...teg.topic-rule-lambda-action.expected.json | 97 +++++++++++++++++++ .../test/integ.topic-rule-lambda-action.ts | 30 ++++++ ....topic-rule-republish-action.expected.json | 87 +++++++++++++++++ .../test/integ.topic-rule-republish-action.ts | 25 +++++ .../aws-iot-actions/test/iot-actions.test.ts | 4 +- 8 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.expected.json create mode 100644 packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.ts create mode 100644 packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.expected.json create mode 100644 packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.ts diff --git a/packages/@aws-cdk/aws-iot-actions/lib/republish.ts b/packages/@aws-cdk/aws-iot-actions/lib/republish.ts index a5228eab9efc8..e06213cc81b32 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/republish.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/republish.ts @@ -1,5 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as iot from '@aws-cdk/aws-iot'; +import { Stack, Arn } from '@aws-cdk/core'; import { singletonTopicRuleRole } from './util'; /** @@ -39,15 +40,19 @@ export class Republish implements iot.ITopicRuleAction { } public bind(rule: iot.ITopicRule): iot.TopicRuleActionConfig { + const stack = Stack.of(rule); // Allow rule to publish to topic const role = this.props.role || singletonTopicRuleRole(rule, [new iam.PolicyStatement({ actions: ['iot:Publish'], - resources: [this.props.topic], + resources: [ + Arn.format({ + resource: 'topic', + service: 'iot', + resourceName: this.props.topic, + }, stack), + ], })]); - // Ensure permission is deployed before rule - rule.node.addDependency(role); - return { republish: { qos: this.props.qos || 0, diff --git a/packages/@aws-cdk/aws-iot-actions/lib/sns.ts b/packages/@aws-cdk/aws-iot-actions/lib/sns.ts index 7af980a8f5344..550a874f7bfe7 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/sns.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/sns.ts @@ -57,9 +57,6 @@ export class Sns implements iot.ITopicRuleAction { resources: [this.props.topic.topicArn], })]); - // Ensure permission is deployed before rule - rule.node.addDependency(role); - return { sns: { messageFormat: this.props.messageFormat || MessageFormats.RAW, diff --git a/packages/@aws-cdk/aws-iot-actions/lib/util.ts b/packages/@aws-cdk/aws-iot-actions/lib/util.ts index 876bb0e6702f0..fa4be2040eca0 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/util.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/util.ts @@ -13,7 +13,7 @@ import { Construct } from '@aws-cdk/core'; */ export function singletonTopicRuleRole(scope: IConstruct, policyStatements: iam.PolicyStatement[]): iam.IRole { const stack = Stack.of(scope); - const id = 'DefaultPolicy'; + const id = 'AllowIot'; const existing = stack.node.tryFindChild(id) as iam.IRole; if (existing) { return existing; } diff --git a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.expected.json new file mode 100644 index 0000000000000..e16abf4bea774 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.expected.json @@ -0,0 +1,97 @@ +{ + "Resources": { + "FunctionServiceRole675BB04A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Function76856677": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "boom" + }, + "Role": { + "Fn::GetAtt": [ + "FunctionServiceRole675BB04A", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "FunctionServiceRole675BB04A" + ] + }, + "FunctionAllowIotD2ECA9CD": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Function76856677", + "Arn" + ] + }, + "Principal": "iot.amazonaws.com", + "SourceAccount": { + "Ref": "AWS::AccountId" + } + } + }, + "MyIotTopicRuleFE4E2C8B": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Lambda": { + "FunctionArn": { + "Fn::GetAtt": [ + "Function76856677", + "Arn" + ] + } + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "Description": "", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'topic/subtopic'" + }, + "RuleName": "TODO" + }, + "DependsOn": [ + "FunctionAllowIotD2ECA9CD" + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.ts b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.ts new file mode 100644 index 0000000000000..0074f9aa98a59 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.ts @@ -0,0 +1,30 @@ +import * as iot from '@aws-cdk/aws-iot'; +import * as lambda from '@aws-cdk/aws-lambda'; + +import * as cdk from '@aws-cdk/core'; +import * as actions from '../lib'; + + +// -------------------------------- +// Define a rule that triggers an Lambda funcion when data is received. +// Automatically creates invoke lambda permission +// +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-topic-rule-lambda-action'); + +// Create an IoT topic rule with an error action. +new iot.TopicRule(stack, 'MyIotTopicRule', { + sql: 'SELECT * FROM \'topic/subtopic\'', + actions: [ + new actions.Lambda({ + function: new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('boom'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_12_X, + }), + }), + ], +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.expected.json new file mode 100644 index 0000000000000..808e5ee276812 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.expected.json @@ -0,0 +1,87 @@ +{ + "Resources": { + "MyIotTopicRuleFE4E2C8B": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Republish": { + "Qos": 0, + "RoleArn": { + "Fn::GetAtt": [ + "MyIotTopicRuleAllowIot2CEDF6FE", + "Arn" + ] + }, + "Topic": "some/topic" + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "Description": "", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'topic/subtopic'" + }, + "RuleName": "TODO" + } + }, + "MyIotTopicRuleAllowIot2CEDF6FE": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyIotTopicRuleAllowIotDefaultPolicy118BC1CA": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "iot:Publish", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iot:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":topic/some/topic" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyIotTopicRuleAllowIotDefaultPolicy118BC1CA", + "Roles": [ + { + "Ref": "MyIotTopicRuleAllowIot2CEDF6FE" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.ts b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.ts new file mode 100644 index 0000000000000..58df19637279b --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.ts @@ -0,0 +1,25 @@ +import * as iot from '@aws-cdk/aws-iot'; + +import * as cdk from '@aws-cdk/core'; +import * as actions from '../lib'; + + +// -------------------------------- +// Define a rule that triggers an Lambda funcion when data is received. +// Automatically creates invoke lambda permission +// +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-topic-rule-lambda-action'); + +// Create an IoT topic rule with an error action. +new iot.TopicRule(stack, 'MyIotTopicRule', { + sql: 'SELECT * FROM \'topic/subtopic\'', + actions: [ + new actions.Republish({ + topic: 'some/topic', + }), + ], +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-iot-actions/test/iot-actions.test.ts b/packages/@aws-cdk/aws-iot-actions/test/iot-actions.test.ts index 9a72968a04358..f8e497ace9831 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/iot-actions.test.ts +++ b/packages/@aws-cdk/aws-iot-actions/test/iot-actions.test.ts @@ -66,7 +66,7 @@ test('add sns action', () => { { Sns: { MessageFormat: 'RAW', - RoleArn: { 'Fn::GetAtt': ['PublishSnsRuleDefaultPolicyF2EC542D', 'Arn'] }, + RoleArn: { 'Fn::GetAtt': ['PublishSnsRuleAllowIot34A25A9A', 'Arn'] }, TargetArn: { Ref: 'MyTopic86869434' }, }, }, @@ -92,7 +92,7 @@ test('add republish action', () => { { Republish: { Qos: 0, - RoleArn: { 'Fn::GetAtt': ['RepublishRuleDefaultPolicy143E0758', 'Arn'] }, + RoleArn: { 'Fn::GetAtt': ['RepublishRuleAllowIotB39A8B3C', 'Arn'] }, Topic: '$$aws/things/MyThing/shadow/update', }, }, From 5b151059c6218945b5c30c9208256e9a9662589a Mon Sep 17 00:00:00 2001 From: Darren Date: Sun, 14 Mar 2021 12:55:45 -0500 Subject: [PATCH 07/32] feat(aws-iot): topic rules and actions add `aws-iot-actions` as dependency --- packages/decdk/package.json | 1 + packages/monocdk/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/decdk/package.json b/packages/decdk/package.json index 4b2ec6dc2fdc0..b88a1f8183aeb 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -119,6 +119,7 @@ "@aws-cdk/aws-imagebuilder": "0.0.0", "@aws-cdk/aws-inspector": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-iot-actions": "0.0.0", "@aws-cdk/aws-iot1click": "0.0.0", "@aws-cdk/aws-iotanalytics": "0.0.0", "@aws-cdk/aws-iotevents": "0.0.0", diff --git a/packages/monocdk/package.json b/packages/monocdk/package.json index 382f47c2be9d0..021deeb05e931 100644 --- a/packages/monocdk/package.json +++ b/packages/monocdk/package.json @@ -198,6 +198,7 @@ "@aws-cdk/aws-imagebuilder": "0.0.0", "@aws-cdk/aws-inspector": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-iot-actions": "0.0.0", "@aws-cdk/aws-iot1click": "0.0.0", "@aws-cdk/aws-iotanalytics": "0.0.0", "@aws-cdk/aws-iotevents": "0.0.0", From 0a73132451b132b3c73bb3cd96a92852d15e53e7 Mon Sep 17 00:00:00 2001 From: Darren Date: Sun, 14 Mar 2021 13:04:22 -0500 Subject: [PATCH 08/32] feat(aws-iot): topic rules and actions --- .../integ.topic-rule-republish-action.expected.json | 12 ++++++------ .../test/integ.topic-rule-republish-action.ts | 7 +++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.expected.json index 808e5ee276812..6b91e7beac44d 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.expected.json +++ b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.expected.json @@ -1,6 +1,6 @@ { "Resources": { - "MyIotTopicRuleFE4E2C8B": { + "MyRepublishTopicRuleD870AE64": { "Type": "AWS::IoT::TopicRule", "Properties": { "TopicRulePayload": { @@ -10,7 +10,7 @@ "Qos": 0, "RoleArn": { "Fn::GetAtt": [ - "MyIotTopicRuleAllowIot2CEDF6FE", + "MyRepublishTopicRuleAllowIotE447F1AF", "Arn" ] }, @@ -26,7 +26,7 @@ "RuleName": "TODO" } }, - "MyIotTopicRuleAllowIot2CEDF6FE": { + "MyRepublishTopicRuleAllowIotE447F1AF": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { @@ -43,7 +43,7 @@ } } }, - "MyIotTopicRuleAllowIotDefaultPolicy118BC1CA": { + "MyRepublishTopicRuleAllowIotDefaultPolicy0E6BCB09": { "Type": "AWS::IAM::Policy", "Properties": { "PolicyDocument": { @@ -75,10 +75,10 @@ ], "Version": "2012-10-17" }, - "PolicyName": "MyIotTopicRuleAllowIotDefaultPolicy118BC1CA", + "PolicyName": "MyRepublishTopicRuleAllowIotDefaultPolicy0E6BCB09", "Roles": [ { - "Ref": "MyIotTopicRuleAllowIot2CEDF6FE" + "Ref": "MyRepublishTopicRuleAllowIotE447F1AF" } ] } diff --git a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.ts b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.ts index 58df19637279b..5f95e2eebe683 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.ts @@ -1,19 +1,18 @@ import * as iot from '@aws-cdk/aws-iot'; - import * as cdk from '@aws-cdk/core'; import * as actions from '../lib'; // -------------------------------- -// Define a rule that triggers an Lambda funcion when data is received. +// Define a rule that triggers to republish received data. // Automatically creates invoke lambda permission // const app = new cdk.App(); -const stack = new cdk.Stack(app, 'aws-cdk-topic-rule-lambda-action'); +const stack = new cdk.Stack(app, 'aws-cdk-topic-rule-republish-action'); // Create an IoT topic rule with an error action. -new iot.TopicRule(stack, 'MyIotTopicRule', { +new iot.TopicRule(stack, 'MyRepublishTopicRule', { sql: 'SELECT * FROM \'topic/subtopic\'', actions: [ new actions.Republish({ From fabc6852b6b31571b08da0bb7eb8c724b1c2e8f7 Mon Sep 17 00:00:00 2001 From: Darren Date: Sun, 14 Mar 2021 13:46:28 -0500 Subject: [PATCH 09/32] feat(aws-iot): topic rules and actions and another one --- packages/aws-cdk-lib/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index 5380837857d8f..00fa40b6d87ca 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -193,6 +193,7 @@ "@aws-cdk/aws-imagebuilder": "0.0.0", "@aws-cdk/aws-inspector": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-iot-actions": "0.0.0", "@aws-cdk/aws-iot1click": "0.0.0", "@aws-cdk/aws-iotanalytics": "0.0.0", "@aws-cdk/aws-iotevents": "0.0.0", From 5bb033002e13cbc294dd519d2bd0d70c58059779 Mon Sep 17 00:00:00 2001 From: Darren Date: Sun, 14 Mar 2021 15:53:30 -0500 Subject: [PATCH 10/32] feat(aws-iot): topic rules and actions use physical name --- packages/@aws-cdk/aws-iot/lib/topic-rule.ts | 2 +- packages/@aws-cdk/aws-iot/test/topic-rule.test.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts index efdffb1888a3b..cf43af9ebfa7e 100644 --- a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts +++ b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts @@ -106,7 +106,7 @@ export class TopicRule extends Resource implements ITopicRule { constructor(scope: Construct, id: string, props: TopicRuleProps) { super(scope, id, { - physicalName: props.ruleName || 'TODO', + physicalName: props.ruleName, }); const resource = new CfnTopicRule(this, 'Resource', { diff --git a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts index a2b454a8b1e30..1f9a4286d5564 100644 --- a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts +++ b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts @@ -26,7 +26,6 @@ nodeunitShim({ RuleDisabled: false, Sql: 'SELECT * FROM \'topic/subtopic\'', }, - RuleName: 'TODO', })); test.done(); }, @@ -54,7 +53,6 @@ nodeunitShim({ RuleDisabled: false, Sql: 'SELECT * FROM \'topic/subtopic\'', }, - RuleName: 'TODO', })); test.done(); }, From 1eda7015f726f7357a7dba134f11182b3ecfdf5b Mon Sep 17 00:00:00 2001 From: Darren Date: Sun, 14 Mar 2021 18:04:36 -0500 Subject: [PATCH 11/32] feat(aws-iot): topic rules and actions handle error action --- packages/@aws-cdk/aws-iot/lib/topic-rule.ts | 16 ++++- packages/@aws-cdk/aws-iot/lib/util.ts | 2 + .../@aws-cdk/aws-iot/test/topic-rule.test.ts | 68 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts index cf43af9ebfa7e..c23f970c09317 100644 --- a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts +++ b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts @@ -2,7 +2,7 @@ import { Resource, IResource, Lazy } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnTopicRule } from './iot.generated'; import { ITopicRuleAction } from './topic-rule-action'; -import { parseRuleName } from './util'; +import { parseRuleName, undefinedIfAllValuesAreEmpty } from './util'; /** * An IoT Topic Rule @@ -103,15 +103,22 @@ export class TopicRule extends Resource implements ITopicRule { public readonly ruleName: string; public readonly topicRuleArn: string; private readonly actions = new Array(); + private errorAction: CfnTopicRule.ActionProperty = {}; constructor(scope: Construct, id: string, props: TopicRuleProps) { super(scope, id, { physicalName: props.ruleName, }); + + if (props.errorAction) { + this.addErrorAction(props.errorAction); + } + const resource = new CfnTopicRule(this, 'Resource', { ruleName: this.physicalName, topicRulePayload: { + errorAction: Lazy.any({ produce: () => undefinedIfAllValuesAreEmpty(this.errorAction) }), description: props.description || '', awsIotSqlVersion: props.awsIotSqlVersion || '2015-10-08', sql: props.sql, @@ -134,6 +141,13 @@ export class TopicRule extends Resource implements ITopicRule { this.actions.push(action.bind(this)); } + /** + * Adds an error action to this topic rule. + */ + public addErrorAction(action: ITopicRuleAction) { + this.errorAction = action.bind(this); + } + public renderActions() { if (this.actions.length === 0) { return undefined; diff --git a/packages/@aws-cdk/aws-iot/lib/util.ts b/packages/@aws-cdk/aws-iot/lib/util.ts index b74eb8551c97a..2e49895a545c4 100644 --- a/packages/@aws-cdk/aws-iot/lib/util.ts +++ b/packages/@aws-cdk/aws-iot/lib/util.ts @@ -1,5 +1,7 @@ import { Fn } from '@aws-cdk/core'; +export { undefinedIfAllValuesAreEmpty } from '@aws-cdk/core/lib/util'; + export function parseRuleName(topicRuleArn: string): string { return Fn.select(1, Fn.split('rule/', topicRuleArn)); } diff --git a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts index 1f9a4286d5564..f5a6027b4cec3 100644 --- a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts +++ b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts @@ -63,6 +63,74 @@ nodeunitShim({ test.deepEqual(topic.ruleName, 'MyTopicRule'); test.done(); }, + 'can construct with error action'(test: Test) { + // GIVEN + const stack = new Stack(); + // WHEN + const rule = new TopicRule(stack, 'IotTopicRule', { + sql: 'SELECT * FROM \'topic/subtopic\'', + errorAction: new DummyAction(), + }); + + rule.addAction(new DummyAction()); + + // THEN + expect(stack).to(haveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [{ + Republish: { + RoleArn: 'arn:iam::::role/MyRole', + Topic: 'topic/subtopic', + }, + }], + AwsIotSqlVersion: '2015-10-08', + Description: '', + ErrorAction: { + Republish: { + RoleArn: 'arn:iam::::role/MyRole', + Topic: 'topic/subtopic', + }, + }, + RuleDisabled: false, + Sql: 'SELECT * FROM \'topic/subtopic\'', + }, + })); + test.done(); + }, + 'can add error action after construction'(test: Test) { + // GIVEN + const stack = new Stack(); + // WHEN + const rule = new TopicRule(stack, 'IotTopicRule', { + sql: 'SELECT * FROM \'topic/subtopic\'', + actions: [new DummyAction()], + }); + + rule.addErrorAction(new DummyAction()); + + // THEN + expect(stack).to(haveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [{ + Republish: { + RoleArn: 'arn:iam::::role/MyRole', + Topic: 'topic/subtopic', + }, + }], + AwsIotSqlVersion: '2015-10-08', + Description: '', + ErrorAction: { + Republish: { + RoleArn: 'arn:iam::::role/MyRole', + Topic: 'topic/subtopic', + }, + }, + RuleDisabled: false, + Sql: 'SELECT * FROM \'topic/subtopic\'', + }, + })); + test.done(); + }, }); class DummyAction implements ITopicRuleAction { From f3d30b7ed38eb06aff975fcafa9b9aeb032c1f89 Mon Sep 17 00:00:00 2001 From: Darren Date: Sun, 14 Mar 2021 18:20:42 -0500 Subject: [PATCH 12/32] feat(aws-iot): topic rules and actions fix description and add SQL version enum --- packages/@aws-cdk/aws-iot/lib/topic-rule.ts | 35 +++++++++++++++++-- .../@aws-cdk/aws-iot/test/topic-rule.test.ts | 4 --- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts index c23f970c09317..afcdd8bba8c98 100644 --- a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts +++ b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts @@ -4,6 +4,35 @@ import { CfnTopicRule } from './iot.generated'; import { ITopicRuleAction } from './topic-rule-action'; import { parseRuleName, undefinedIfAllValuesAreEmpty } from './util'; +/** + * The AWS IoT rules engine uses an SQL-like syntax to select data from MQTT + * messages. The SQL statements are interpreted based on an SQL version + * specified with the awsIotSqlVersion property in a JSON document that describes + * the rule. + */ +export enum AwsIotSqlVersion { + /** + * Version beta + * + * The most recent beta SQL version. If you use this version, it might + * introduce breaking changes to your rules. + */ + BETA = 'beta', + /** + * Version 2015-10-08 + * + * The original SQL version built on 2015-10-08. + */ + VERSION2015_10_08 = '2015-10-08', + /** + * Version 2016-03-23 + * + * The SQL version built on 2016-03-23. + * + * Supports secrets, task recycling. + */ + VERSION2016_03_23 = '2016-03-23', +} /** * An IoT Topic Rule */ @@ -61,7 +90,7 @@ export interface TopicRuleProps { * * @default - 2012-17-10 */ - readonly awsIotSqlVersion?: string; + readonly awsIotSqlVersion?: AwsIotSqlVersion; /** * The topic rule description. * @@ -119,8 +148,8 @@ export class TopicRule extends Resource implements ITopicRule { ruleName: this.physicalName, topicRulePayload: { errorAction: Lazy.any({ produce: () => undefinedIfAllValuesAreEmpty(this.errorAction) }), - description: props.description || '', - awsIotSqlVersion: props.awsIotSqlVersion || '2015-10-08', + description: props.description, + awsIotSqlVersion: props.awsIotSqlVersion || AwsIotSqlVersion.VERSION2015_10_08, sql: props.sql, ruleDisabled: props.ruleDisabled || false, actions: Lazy.any({ produce: () => this.renderActions() }), diff --git a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts index f5a6027b4cec3..d3942a01662ce 100644 --- a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts +++ b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts @@ -22,7 +22,6 @@ nodeunitShim({ }, }], AwsIotSqlVersion: '2015-10-08', - Description: '', RuleDisabled: false, Sql: 'SELECT * FROM \'topic/subtopic\'', }, @@ -49,7 +48,6 @@ nodeunitShim({ }, }], AwsIotSqlVersion: '2015-10-08', - Description: '', RuleDisabled: false, Sql: 'SELECT * FROM \'topic/subtopic\'', }, @@ -84,7 +82,6 @@ nodeunitShim({ }, }], AwsIotSqlVersion: '2015-10-08', - Description: '', ErrorAction: { Republish: { RoleArn: 'arn:iam::::role/MyRole', @@ -118,7 +115,6 @@ nodeunitShim({ }, }], AwsIotSqlVersion: '2015-10-08', - Description: '', ErrorAction: { Republish: { RoleArn: 'arn:iam::::role/MyRole', From d13366d6a92adb2dffb4240ac800a6cdccd2191f Mon Sep 17 00:00:00 2001 From: Darren Date: Sun, 14 Mar 2021 21:36:43 -0500 Subject: [PATCH 13/32] feat(aws-iot): topic rules and actions add updated integrations --- .../test/integ.topic-rule-lambda-action.expected.json | 4 +--- .../test/integ.topic-rule-republish-action.expected.json | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.expected.json index e16abf4bea774..27b50af4c8f86 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.expected.json +++ b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.expected.json @@ -83,11 +83,9 @@ } ], "AwsIotSqlVersion": "2015-10-08", - "Description": "", "RuleDisabled": false, "Sql": "SELECT * FROM 'topic/subtopic'" - }, - "RuleName": "TODO" + } }, "DependsOn": [ "FunctionAllowIotD2ECA9CD" diff --git a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.expected.json index 6b91e7beac44d..4314409e7716f 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.expected.json +++ b/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.expected.json @@ -19,11 +19,9 @@ } ], "AwsIotSqlVersion": "2015-10-08", - "Description": "", "RuleDisabled": false, "Sql": "SELECT * FROM 'topic/subtopic'" - }, - "RuleName": "TODO" + } } }, "MyRepublishTopicRuleAllowIotE447F1AF": { From 5f98cc0dae11b7dbb6a4700e61202d5c5a266a10 Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Tue, 16 Mar 2021 11:43:37 +0100 Subject: [PATCH 14/32] chore(region-info): metadata service is in eu-south-1, af-south-1 (#13598) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/region-info/build-tools/fact-tables.ts | 4 ++-- .../region-info/test/__snapshots__/region-info.test.js.snap | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 2f47e02f64da4..c2ce689f3aaf3 100644 --- a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts +++ b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts @@ -19,7 +19,7 @@ export const AWS_CDK_METADATA = new Set([ // 'us-gov-west-1', // 'us-iso-east-1', // 'us-isob-east-1', - // 'af-south-1', + 'af-south-1', 'ap-south-1', 'ap-east-1', // 'ap-northeast-3', @@ -35,7 +35,7 @@ export const AWS_CDK_METADATA = new Set([ 'eu-west-2', 'eu-west-3', 'eu-north-1', - // 'eu-south-1', + 'eu-south-1', 'me-south-1', 'sa-east-1', ]); 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 ab12430e57c84..eb7c82b6eb28d 100644 --- a/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap +++ b/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap @@ -3,7 +3,7 @@ exports[`built-in data is correct 1`] = ` Object { "af-south-1": Object { - "cdkMetadataResourceAvailable": false, + "cdkMetadataResourceAvailable": true, "domainSuffix": "amazonaws.com", "partition": "aws", "s3StaticWebsiteEndpoint": "s3-website.af-south-1.amazonaws.com", @@ -263,7 +263,7 @@ Object { "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", }, "eu-south-1": Object { - "cdkMetadataResourceAvailable": false, + "cdkMetadataResourceAvailable": true, "domainSuffix": "amazonaws.com", "partition": "aws", "s3StaticWebsiteEndpoint": "s3-website.eu-south-1.amazonaws.com", From 9fee1e97f97fef98c4f0e401cbd0d5274451b731 Mon Sep 17 00:00:00 2001 From: Darren Date: Tue, 16 Mar 2021 08:28:00 -0500 Subject: [PATCH 15/32] fix(ec2): Security Groups support all protocols (#13593) Satisfies #13497 to close #13403 ---- *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-ec2/lib/port.ts | 151 +++++++++++++++++- packages/@aws-cdk/aws-ec2/package.json | 148 ++++++++++++++++- .../aws-ec2/test/integ.vpc.expected.json | 14 -- packages/@aws-cdk/aws-ec2/test/integ.vpc.ts | 2 - 4 files changed, 290 insertions(+), 25 deletions(-) diff --git a/packages/@aws-cdk/aws-ec2/lib/port.ts b/packages/@aws-cdk/aws-ec2/lib/port.ts index 314c8d615b0dd..8436f3455cda1 100644 --- a/packages/@aws-cdk/aws-ec2/lib/port.ts +++ b/packages/@aws-cdk/aws-ec2/lib/port.ts @@ -2,17 +2,158 @@ import { Token } from '@aws-cdk/core'; /** * Protocol for use in Connection Rules + * + * https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml */ export enum Protocol { ALL = '-1', + HOPOPT = '0', + ICMP = 'icmp', + IGMP = '2', + GGP = '3', + IPV4 = '4', + ST = '5', TCP = 'tcp', + CBT = '7', + EGP = '8', + IGP = '9', + BBN_RCC_MON = '10', + NVP_II = '11', + PUP = '12', + EMCON = '14', + XNET = '15', + CHAOS = '16', UDP = 'udp', - ICMP = 'icmp', - ICMPV6 = '58', - ESP = 'esp', - AH = 'ah', + MUX = '18', + DCN_MEAS = '19', + HMP = '20', + PRM = '21', + XNS_IDP = '22', + TRUNK_1 = '23', + TRUNK_2 = '24', + LEAF_1 = '25', + LEAF_2 = '26', + RDP = '27', + IRTP = '28', + ISO_TP4 = '29', + NETBLT = '30', + MFE_NSP = '31', + MERIT_INP = '32', + DCCP = '33', + THREEPC = '34', + IDPR = '35', + XTP = '36', + DDP = '37', + IDPR_CMTP = '38', + TPPLUSPLUS = '39', + IL = '40', + IPV6 = '41', + SDRP = '42', + IPV6_ROUTE = '43', + IPV6_FRAG = '44', + IDRP = '45', + RSVP = '46', + GRE = '47', + DSR = '48', + BNA = '49', + ESP = '50', + AH = '51', + I_NLSP = '52', + SWIPE = '53', + NARP = '54', + MOBILE = '55', + TLSP = '56', + SKIP = '57', + ICMPV6 = 'icmpv6', + IPV6_NONXT = '59', + IPV6_OPTS = '60', + CFTP = '62', + ANY_LOCAL = '63', + SAT_EXPAK = '64', + KRYPTOLAN = '65', + RVD = '66', + IPPC = '67', + ANY_DFS = '68', + SAT_MON = '69', + VISA = '70', + IPCV = '71', + CPNX = '72', + CPHB = '73', + WSN = '74', + PVP = '75', + BR_SAT_MON = '76', + SUN_ND = '77', + WB_MON = '78', + WB_EXPAK = '79', + ISO_IP = '80', + VMTP = '81', + SECURE_VMTP = '82', + VINES = '83', + TTP = '84', + IPTM = '84', + NSFNET_IGP = '85', + DGP = '86', + TCF = '87', + EIGRP = '88', + OSPFIGP = '89', + SPRITE_RPC = '90', + LARP = '91', + MTP = '92', + AX_25 = '93', + IPIP = '94', + MICP = '95', + SCC_SP = '96', + ETHERIP = '97', + ENCAP = '98', + ANY_ENC = '99', + GMTP = '100', + IFMP = '101', + PNNI = '102', + PIM = '103', + ARIS = '104', + SCPS = '105', + QNX = '106', + A_N = '107', + IPCOMP = '108', + SNP = '109', + COMPAQ_PEER = '110', + IPX_IN_IP = '111', + VRRP = '112', + PGM = '113', + ANY_0_HOP = '114', + L2_T_P = '115', + DDX = '116', + IATP = '117', + STP = '118', + SRP = '119', + UTI = '120', + SMP = '121', + SM = '122', + PTP = '123', + ISIS_IPV4 = '124', + FIRE = '125', + CRTP = '126', + CRUDP = '127', + SSCOPMCE = '128', + IPLT = '129', + SPS = '130', + PIPE = '131', + SCTP = '132', + FC = '133', + RSVP_E2E_IGNORE = '134', + MOBILITY_HEADER = '135', + UDPLITE = '136', + MPLS_IN_IP = '137', + MANET = '138', + HIP = '139', + SHIM6 = '140', + WESP = '141', + ROHC = '142', + ETHERNET = '143', + EXPERIMENT_1 = '253', + EXPERIMENT_2 = '254', + RESERVED = '255', } - /** * Properties to create a port range */ diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index c5d5d9be6a64e..c32f701574ffc 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -314,13 +314,153 @@ "docs-public-apis:@aws-cdk/aws-ec2.AmazonLinuxStorage", "docs-public-apis:@aws-cdk/aws-ec2.OperatingSystemType.LINUX", "docs-public-apis:@aws-cdk/aws-ec2.OperatingSystemType.WINDOWS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.AH", "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ALL", - "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TCP", - "docs-public-apis:@aws-cdk/aws-ec2.Protocol.UDP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ANY_0_HOP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ANY_DFS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ANY_ENC", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ANY_LOCAL", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ARIS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.AX_25", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.A_N", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.BBN_RCC_MON", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.BNA", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.BR_SAT_MON", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.CBT", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.CFTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.CHAOS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.COMPAQ_PEER", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.CPHB", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.CPNX", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.CRTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.CRUDP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.DCCP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.DCN_MEAS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.DDP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.DDX", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.DGP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.DSR", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.EGP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.EIGRP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.EMCON", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ENCAP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ESP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ETHERIP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ETHERNET", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.EXPERIMENT_1", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.EXPERIMENT_2", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.FC", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.FIRE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.GGP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.GMTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.GRE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.HIP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.HMP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.HOPOPT", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IATP", "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ICMP", "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ICMPV6", - "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ESP", - "docs-public-apis:@aws-cdk/aws-ec2.Protocol.AH", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IDPR", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IDPR_CMTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IDRP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IFMP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IGMP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IGP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IL", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPCOMP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPCV", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPIP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPLT", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPPC", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPTM", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPV4", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPV6", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPV6_FRAG", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPV6_NONXT", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPV6_OPTS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPV6_ROUTE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IPX_IN_IP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.IRTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ISIS_IPV4", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ISO_IP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ISO_TP4", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.I_NLSP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.KRYPTOLAN", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.L2_T_P", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.LARP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.LEAF_1", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.LEAF_2", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MANET", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MERIT_INP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MFE_NSP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MICP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MOBILE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MOBILITY_HEADER", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MPLS_IN_IP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.MUX", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.NARP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.NETBLT", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.NSFNET_IGP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.NVP_II", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.OSPFIGP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PGM", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PIM", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PIPE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PNNI", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PRM", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PUP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.PVP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.QNX", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.RDP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.RESERVED", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ROHC", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.RSVP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.RSVP_E2E_IGNORE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.RVD", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SAT_EXPAK", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SAT_MON", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SCC_SP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SCPS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SCTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SDRP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SECURE_VMTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SHIM6", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SKIP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SM", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SMP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SNP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SPRITE_RPC", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SPS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SRP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SSCOPMCE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ST", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.STP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SUN_ND", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.SWIPE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TCF", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TCP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.THREEPC", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TLSP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TPPLUSPLUS", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TRUNK_1", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TRUNK_2", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.TTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.UDP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.UDPLITE", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.UTI", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.VINES", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.VISA", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.VMTP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.VRRP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.WB_EXPAK", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.WB_MON", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.WESP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.WSN", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.XNET", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.XNS_IDP", + "docs-public-apis:@aws-cdk/aws-ec2.Protocol.XTP", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2008_SP2_ENGLISH_64BIT_SQL_2008_SP4_EXPRESS", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_CHINESE_SIMPLIFIED_64BIT_BASE", "docs-public-apis:@aws-cdk/aws-ec2.WindowsVersion.WINDOWS_SERVER_2012_R2_RTM_CHINESE_TRADITIONAL_64BIT_BASE", diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json index 641b97b4ddbd5..8aad8918d8ace 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc.expected.json @@ -567,20 +567,6 @@ "FromPort": 800, "IpProtocol": "udp", "ToPort": 801 - }, - { - "CidrIp": "0.0.0.0/0", - "Description": "from 0.0.0.0/0:ESP 50", - "FromPort": 50, - "IpProtocol": "esp", - "ToPort": 50 - }, - { - "CidrIp": "0.0.0.0/0", - "Description": "from 0.0.0.0/0:AH 51", - "FromPort": 51, - "IpProtocol": "ah", - "ToPort": 51 } ], "VpcId": { diff --git a/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts b/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts index 88e4dacf9839a..2ffd5653e33f4 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts +++ b/packages/@aws-cdk/aws-ec2/test/integ.vpc.ts @@ -16,8 +16,6 @@ const rules = [ ec2.Port.allUdp(), ec2.Port.udp(123), ec2.Port.udpRange(800, 801), - ec2.Port.esp(), - ec2.Port.ah(), ]; for (const rule of rules) { From 93da6a36c13c173ccfd5b935f4021769ef7f84ef Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Tue, 16 Mar 2021 13:52:35 +0000 Subject: [PATCH 16/32] chore(release): 1.94.0 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ version.v1.json | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca6ebeb1b8572..9cc821cbde1c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,36 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.94.0](https://github.com/aws/aws-cdk/compare/v1.93.0...v1.94.0) (2021-03-16) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **appmesh:** Backend, backend default and Virtual Service client policies structures are being altered +* **appmesh**: you must use the backend default interface to define backend defaults in `VirtualGateway`. + The property name also changed from `backendsDefaultClientPolicy` to `backendDefaults` +* **appmesh**: you must use the backend default interface to define backend defaults in `VirtualNode`, + (the property name also changed from `backendsDefaultClientPolicy` to `backendDefaults`), + and the `Backend` class to define a backend +* **appmesh**: you can no longer attach a client policy to a `VirtualService` + +### Features + +* **appmesh:** add missing route match features ([#13350](https://github.com/aws/aws-cdk/issues/13350)) ([b71efd9](https://github.com/aws/aws-cdk/commit/b71efd9d12843ab4b495d53e565cec97d60748f3)), closes [#11645](https://github.com/aws/aws-cdk/issues/11645) +* **aws-elasticloadbalancingv2:** add protocol version for ALB TargetGroups ([#13570](https://github.com/aws/aws-cdk/issues/13570)) ([165a3d8](https://github.com/aws/aws-cdk/commit/165a3d877b7ab23f29e42e1e74ee7c5cb35b7f24)), closes [#12869](https://github.com/aws/aws-cdk/issues/12869) +* **ecs-patterns:** Add ECS deployment circuit breaker support to higher-level constructs ([#12719](https://github.com/aws/aws-cdk/issues/12719)) ([e80a98a](https://github.com/aws/aws-cdk/commit/e80a98aa8839e9b9b89701158d82b991e9ebaa65)), closes [#12534](https://github.com/aws/aws-cdk/issues/12534) [#12360](https://github.com/aws/aws-cdk/issues/12360) + + +### Bug Fixes + +* **appmesh:** Move Client Policy from Virtual Service to backend structure ([#12943](https://github.com/aws/aws-cdk/issues/12943)) ([d3f4284](https://github.com/aws/aws-cdk/commit/d3f428435976c55ca950279cfc841665fd504370)), closes [#11996](https://github.com/aws/aws-cdk/issues/11996) +* **autoscaling:** AutoScaling on percentile metrics doesn't work ([#13366](https://github.com/aws/aws-cdk/issues/13366)) ([46114bb](https://github.com/aws/aws-cdk/commit/46114bb1f4702019a8873b9162d0a9f10763bc61)), closes [#13144](https://github.com/aws/aws-cdk/issues/13144) +* **cloudwatch:** cannot create Alarms from labeled metrics that start with a digit ([#13560](https://github.com/aws/aws-cdk/issues/13560)) ([278029f](https://github.com/aws/aws-cdk/commit/278029f25b41d956091835364e5a8de91429712c)), closes [#13434](https://github.com/aws/aws-cdk/issues/13434) +* use NodeJS 14 for all packaged custom resources ([#13488](https://github.com/aws/aws-cdk/issues/13488)) ([20a2820](https://github.com/aws/aws-cdk/commit/20a2820ee4d022663fcd0928fbc0f61153ae953f)), closes [#13534](https://github.com/aws/aws-cdk/issues/13534) [#13484](https://github.com/aws/aws-cdk/issues/13484) +* **ec2:** Security Groups support all protocols ([#13593](https://github.com/aws/aws-cdk/issues/13593)) ([8c6b3eb](https://github.com/aws/aws-cdk/commit/8c6b3ebea464e27f68ffcab32857d8baec29c413)), closes [#13403](https://github.com/aws/aws-cdk/issues/13403) +* **lambda:** fromDockerBuild output is located under /asset ([#13539](https://github.com/aws/aws-cdk/issues/13539)) ([77449f6](https://github.com/aws/aws-cdk/commit/77449f61e7075fef1240fc52becb8ea60b9ea9ad)), closes [#13439](https://github.com/aws/aws-cdk/issues/13439) +* **region-info:** ap-northeast-3 data not correctly registered ([#13564](https://github.com/aws/aws-cdk/issues/13564)) ([64da84b](https://github.com/aws/aws-cdk/commit/64da84be5c60bb8132551bcc27a7ca9c7effe95d)), closes [#13561](https://github.com/aws/aws-cdk/issues/13561) + ## [1.93.0](https://github.com/aws/aws-cdk/compare/v1.92.0...v1.93.0) (2021-03-11) diff --git a/version.v1.json b/version.v1.json index 097cc55f8cc18..3280de4395415 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.93.0" + "version": "1.94.0" } From e1df36e310a045f54fe5c2a102f197f77f939f12 Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Wed, 17 Mar 2021 00:16:23 +0200 Subject: [PATCH 17/32] fix(s3): Notifications fail to deploy due to incompatible node runtime (#13624) This [PR](https://github.com/aws/aws-cdk/pull/13488) upgraded our Node runtimes from `10` to `14`. The problem is that Node14 isn't supported for lambda functions using inline code (i.e `ZipFile`). Change to Node12 specifically for the notification handler since it's the only one using `InlineLambda`. Fixes https://github.com/aws/aws-cdk/issues/13620 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-lambda-event-sources/test/integ.s3.expected.json | 2 +- .../aws-s3-notifications/test/integ.notifications.expected.json | 2 +- .../test/lambda/integ.bucket-notifications.expected.json | 2 +- .../test/sns/integ.sns-bucket-notifications.expected.json | 2 +- .../test/sqs/integ.bucket-notifications.expected.json | 2 +- .../notifications-resource/notifications-resource-handler.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json index e4b5d64e2d04e..96f5a3e6a63d7 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/integ.s3.expected.json @@ -185,7 +185,7 @@ "Arn" ] }, - "Runtime": "nodejs14.x", + "Runtime": "nodejs12.x", "Timeout": 300 }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-s3-notifications/test/integ.notifications.expected.json b/packages/@aws-cdk/aws-s3-notifications/test/integ.notifications.expected.json index f0babb81fc0a6..f8b1a350486f9 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/integ.notifications.expected.json +++ b/packages/@aws-cdk/aws-s3-notifications/test/integ.notifications.expected.json @@ -220,7 +220,7 @@ "Arn" ] }, - "Runtime": "nodejs14.x", + "Runtime": "nodejs12.x", "Timeout": 300 }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-s3-notifications/test/lambda/integ.bucket-notifications.expected.json b/packages/@aws-cdk/aws-s3-notifications/test/lambda/integ.bucket-notifications.expected.json index 918c4a9d09334..8eb330f53a91e 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/lambda/integ.bucket-notifications.expected.json +++ b/packages/@aws-cdk/aws-s3-notifications/test/lambda/integ.bucket-notifications.expected.json @@ -244,7 +244,7 @@ "Arn" ] }, - "Runtime": "nodejs14.x", + "Runtime": "nodejs12.x", "Timeout": 300 }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-s3-notifications/test/sns/integ.sns-bucket-notifications.expected.json b/packages/@aws-cdk/aws-s3-notifications/test/sns/integ.sns-bucket-notifications.expected.json index ecdd9d831e74f..2cd8f17706a09 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/sns/integ.sns-bucket-notifications.expected.json +++ b/packages/@aws-cdk/aws-s3-notifications/test/sns/integ.sns-bucket-notifications.expected.json @@ -203,7 +203,7 @@ "Arn" ] }, - "Runtime": "nodejs14.x", + "Runtime": "nodejs12.x", "Timeout": 300 }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-s3-notifications/test/sqs/integ.bucket-notifications.expected.json b/packages/@aws-cdk/aws-s3-notifications/test/sqs/integ.bucket-notifications.expected.json index ea40221e82f15..0e8a6a56cfb6e 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/sqs/integ.bucket-notifications.expected.json +++ b/packages/@aws-cdk/aws-s3-notifications/test/sqs/integ.bucket-notifications.expected.json @@ -192,7 +192,7 @@ "Arn" ] }, - "Runtime": "nodejs14.x", + "Runtime": "nodejs12.x", "Timeout": 300 }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts index 8a47247be1b1e..b013bc0ebd5b8 100644 --- a/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts +++ b/packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts @@ -82,7 +82,7 @@ export class NotificationsResourceHandler extends Construct { Code: { ZipFile: `exports.handler = ${handler.toString()};` }, Handler: 'index.handler', Role: role.roleArn, - Runtime: 'nodejs14.x', + Runtime: 'nodejs12.x', Timeout: 300, }, }); From de953eac0744bc599fd98ddc78b4ffba83c1a66a Mon Sep 17 00:00:00 2001 From: epolon Date: Wed, 17 Mar 2021 00:49:53 +0200 Subject: [PATCH 18/32] chore(release): 1.94.1 --- CHANGELOG.md | 7 +++++++ version.v1.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cc821cbde1c3..392f795c1771e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [1.94.1](https://github.com/aws/aws-cdk/compare/v1.94.0...v1.94.1) (2021-03-16) + + +### Bug Fixes + +* **s3:** Notifications fail to deploy due to incompatible node runtime ([#13624](https://github.com/aws/aws-cdk/issues/13624)) ([26bc3d4](https://github.com/aws/aws-cdk/commit/26bc3d4951a96a4bdf3e3e10464a4e3b80ed563f)) + ## [1.94.0](https://github.com/aws/aws-cdk/compare/v1.93.0...v1.94.0) (2021-03-16) diff --git a/version.v1.json b/version.v1.json index 3280de4395415..0a93d433950d0 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.94.0" + "version": "1.94.1" } From 38c0f5b224a53e3b8f35f27e218670ec2a90c3c3 Mon Sep 17 00:00:00 2001 From: epolon Date: Wed, 17 Mar 2021 00:51:26 +0200 Subject: [PATCH 19/32] fix changelog heading --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 392f795c1771e..a9fe72678f70a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -### [1.94.1](https://github.com/aws/aws-cdk/compare/v1.94.0...v1.94.1) (2021-03-16) +## [1.94.1](https://github.com/aws/aws-cdk/compare/v1.94.0...v1.94.1) (2021-03-16) ### Bug Fixes @@ -77,7 +77,7 @@ All notable changes to this project will be documented in this file. See [standa ## [1.92.0](https://github.com/aws/aws-cdk/compare/v1.91.0...v1.92.0) (2021-03-06) -* **ecs-patterns**: the `desiredCount` property stored on the above constructs will be optional, allowing them to be undefined. This is enabled through the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` feature flag. We would recommend all CDK users to set the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` flag to `true` for all of their existing applications. +* **ecs-patterns**: the `desiredCount` property stored on the above constructs will be optional, allowing them to be undefined. This is enabled through the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` feature flag. We would recommend all CDK users to set the `@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount` flag to `true` for all of their existing applications. ### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES From d253d5cb23acaead419b247bed7d8144e0eec69e Mon Sep 17 00:00:00 2001 From: epolon Date: Wed, 17 Mar 2021 00:51:53 +0200 Subject: [PATCH 20/32] Error instead of error --- scripts/bump.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bump.js b/scripts/bump.js index 5d8b4d63a62d0..bcbe8785c436b 100755 --- a/scripts/bump.js +++ b/scripts/bump.js @@ -11,7 +11,7 @@ const forTesting = process.env.BUMP_CANDIDATE || false; async function main() { if (releaseAs !== 'minor' && releaseAs !== 'patch') { - throw new error(`invalid bump type "${releaseAs}". only "minor" (the default) and "patch" are allowed. major version bumps require *slightly* more intention`); + throw new Error(`invalid bump type "${releaseAs}". only "minor" (the default) and "patch" are allowed. major version bumps require *slightly* more intention`); } console.error(`Starting ${releaseAs} version bump`); From dbf63d238070705df6ffadbe2ee57afed3ba1d99 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 16 Mar 2021 15:07:06 +0100 Subject: [PATCH 21/32] chore(cloudformation-include): build fails on .DS_Store (#13595) The build script was assuming everything in the package directory was itself a directory; not necessarily true on macOS machines. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/cloudformation-include/build.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/@aws-cdk/cloudformation-include/build.js b/packages/@aws-cdk/cloudformation-include/build.js index ab36ffd345d7f..454a10a5014d5 100644 --- a/packages/@aws-cdk/cloudformation-include/build.js +++ b/packages/@aws-cdk/cloudformation-include/build.js @@ -29,6 +29,8 @@ async function main() { for (const constructLibraryDir of constructLibrariesDirs) { const absConstructLibraryDir = path.resolve(constructLibrariesRoot, constructLibraryDir); + if (!fs.statSync(absConstructLibraryDir).isDirectory()) { continue; } // .DS_Store + const libraryPackageJson = require(path.join(absConstructLibraryDir, 'package.json')); const libraryDependencyVersion = dependencies[libraryPackageJson.name]; From 415d9e3a79d9be28081a76863543b8b7e716b39d Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Tue, 16 Mar 2021 14:51:50 +0000 Subject: [PATCH 22/32] chore: update and simplify contribution guide (#13525) I've received feedback from some folks who would like to start contributing to the CDK but found the contribution guide unwieldy. Attempting to wittle down and simplify. Move content around so that useful information is at the top. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- CONTRIBUTING.md | 876 ++++++------------ .../DESIGN_GUIDELINES.md | 0 docs/release.md | 53 ++ 3 files changed, 354 insertions(+), 575 deletions(-) rename DESIGN_GUIDELINES.md => docs/DESIGN_GUIDELINES.md (100%) create mode 100644 docs/release.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7615d7b10db4e..fab4ce03200c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,40 +7,28 @@ and let us know if it's not up-to-date (even better, submit a PR with your corr - [Getting Started](#getting-started) - [Pull Requests](#pull-requests) - - [Pull Request Checklist](#pull-request-checklist) - - [Step 1: Open Issue](#step-1-open-issue) + - [Step 1: Find something to work on](#step-1-find-something-to-work-on) - [Step 2: Design (optional)](#step-2-design-optional) - [Step 3: Work your Magic](#step-3-work-your-magic) - [Step 4: Commit](#step-4-commit) - [Step 5: Pull Request](#step-5-pull-request) - [Step 6: Merge](#step-6-merge) - [Breaking Changes](#breaking-changes) +- [Documentation](#documentation) + - [rosetta](#rosetta) - [Tools](#tools) - - [Main build scripts](#main-build-scripts) - - [Partial build tools](#partial-build-tools) - - [Useful aliases](#useful-aliases) - [Linters](#linters) - [cfn2ts](#cfn2ts) - [scripts/foreach.sh](#scriptsforeachsh) - [Jetbrains support (WebStorm/IntelliJ)](#jetbrains-support-webstormintellij) -- [Workflows](#workflows) - - [Full clean build](#full-clean-build) - - [Full Docker build](#full-docker-build) - - [Partial build](#partial-build) - - [Partial pack](#partial-pack) - - [Quick Iteration](#quick-iteration) - [Linking against this repository](#linking-against-this-repository) - [Running integration tests in parallel](#running-integration-tests-in-parallel) - [Visualizing dependencies in a CloudFormation Template](#visualizing-dependencies-in-a-cloudformation-template) - - [Adding Dependencies](#adding-dependencies) - - [Finding dependency cycles between packages](#finding-dependency-cycles-between-packages) - - [Updating all Dependencies](#updating-all-dependencies) - - [Running CLI integration tests](#running-cli-integration-tests) - - [Changing the Cloud Assembly Schema](#changing-cloud-assembly-schema) - - [API Compatibility Checks](#api-compatibility-checks) - - [Examples](#examples) - - [Feature Flags](#feature-flags) - - [Versioning and Release](#versioning-and-release) + - [Find dependency cycles between packages](#find-dependency-cycles-between-packages) +- [Running CLI integration tests](#running-cli-integration-tests) +- [Changing the Cloud Assembly Schema](#changing-cloud-assembly-schema) +- [Feature Flags](#feature-flags) +- [Versioning and Release](#versioning-and-release) - [Troubleshooting](#troubleshooting) - [Debugging](#debugging) - [Connecting the VS Code Debugger](#connecting-the-vs-code-debugger) @@ -49,134 +37,183 @@ and let us know if it's not up-to-date (even better, submit a PR with your corr ## Getting Started -### Gitpod +The following steps describe how to set up the AWS CDK repository on your local machine. +The alternative is to use [Gitpod](https://www.gitpod.io/), a Cloud IDE for your development. +See [Gitpod section](#gitpod) on how to set up the CDK repo on Gitpod. -For setting up a local development environment, -we recommend using [Gitpod](http://gitpod.io) - -a service that allows you to spin up an in-browser -Visual Studio Code-compatible editor, -with everything set up and ready to go for CDK development. -Just click the button below to create your private workspace: - -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/aws/aws-cdk) - -This will start a new Gitpod workspace, -and immediately kick off a build of the CDK code. -Once it's done (it takes around an hour, unfortunately), -you can work on any package that you want to modify, -as described in ['Quick Iteration'](#quick-iteration) below. - -Gitpod is free for 50 hours per month - -make sure to stop your workspace when you're done -(you can always resume it later, and it won't need to run the build again). - -### Local dependencies +### Setup -If you don't want to use Gitpod, -you need to have the following SDKs and tools locally: +The following tools need to be installed on your system prior to installing the CDK: - [Node.js >= 10.13.0](https://nodejs.org/download/release/latest-v10.x/) - We recommend using a version in [Active LTS](https://nodejs.org/en/about/releases/) - ⚠️ versions `13.0.0` to `13.6.0` are not supported due to compatibility issues with our dependencies. - [Yarn >= 1.19.1, < 2](https://yarnpkg.com/lang/en/docs/install) -- [Java >= OpenJDK 8, 11, 14](https://docs.aws.amazon.com/corretto/latest/corretto-8-ug/downloads-list.html) -- [Apache Maven >= 3.6.0, < 4.0](http://maven.apache.org/install.html) - [.NET Core SDK 3.1.x](https://www.microsoft.com/net/download) - [Python >= 3.6.5, < 4.0](https://www.python.org/downloads/release/python-365/) -- [Docker >= 19.03](https://docs.docker.com/get-docker/) -The basic commands to get the repository cloned and built locally follow: +Run the following commands to clone the repository locally. ```console $ git clone https://github.com/aws/aws-cdk.git $ cd aws-cdk +$ yarn install +``` + +We recommend that you use [Visual Studio Code](https://code.visualstudio.com/) to work on the CDK. +We use `eslint` to keep our consistent in terms of style and reducing defects. We recommend installing the +the [eslint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) as well. + +### Repo Layout + +The AWS CDK is a [NPM](https://www.npmjs.com/about) project written in [typescript](https://www.typescriptlang.org/). +More specifically, it is a [monorepo managed using lerna](https://github.com/lerna/lerna#about). +If you're unfamiliar with any of these technologies, it is useful to learn about them and will make understanding the +AWS CDK codebase easier but strictly not necessary for simple contributions. + +The CDK uses [jsii](https://github.com/aws/jsii/) as its primary build system. jsii enables us to write +typescript-compliant source code and produce polyglot libraries, such as, in Java, .NET, Python and Go. + +The repo contains `packages/` directory that contains the CDK public modules. The source code for the IAM module in the +CDK can be found at the location `packages/@aws-cdk/aws-iam`. +The repo also contains the `tools/` directory that holds custom build tooling (modeled as private npm packages) +specific to the CDK. + +### Build + +The full build of the CDK takes a long time complete; 1-2 hours depending on the performance of the build machine. +However, most first time contributions will require changing only one CDK module, sometimes two. A full build of the +CDK is not required in these cases. + +If you want to work on the `@aws-cdk/aws-ec2` module, the following command will build just the EC2 module and any +necessary dependencies. + +```console +$ cd packages/@aws-cdk/aws-ec2 +$ ../../../scripts/buildup +``` + +Note: The `buildup` command is resumable. If your build fails, you can fix the issue and run `buildup --resume` to +resume. + +At this point, you can run build and test the `aws-ec2` module by running + +```console +$ cd packages/@aws-cdk/aws-ec2 $ yarn build +$ yarn test ``` -If you get compiler errors when building, a common cause is a globally installed typescript. Try uninstalling it. +However, if you wish to build the the entire repository, the following command will achieve this. +```console +cd +yarn build ``` -npm uninstall -g typescript + +You are now ready to start contributing to the CDK. See the [Pull Requests](#pull-requests) section on how to make your +changes and submit it as a pull request. + +### Pack + +As called out in the above sections, the AWS CDK uses jsii to produce polyglot targets. This means that each CDK module +produces artifact in all of its target languages. + +Packing involves generating CDK code in the various target languages and packaging them up to be published to their +respective package managers. Once in a while, these will need to be generated either to test the experience of a new +feature, or reproduce a packaging failure. + +To package a specific module, say the `@aws-cdk/aws-ec2` module: + +```console +$ cd +$ docker run --rm --net=host -it -v $PWD:$PWD -w $PWD jsii/superchain +docker$ cd packages/@aws-cdk/aws-ec2 +docker$ ../../../scripts/foreach.sh --up yarn run package +docker$ exit ``` -Alternatively, the [Full Docker build](#full-docker-build) workflow can be used so -that you don't have to worry about installing all those tools on your local machine -and instead only depend on having a working Docker install. +The `dist/` folder within each module contains the packaged up language artifacts. -## Pull Requests +## Docker Build (Alternative) -### Pull Request Checklist - -* [ ] Testing - - Unit test added (prefer not to modify an existing test, otherwise, it's probably a breaking change) - - __CLI change?:__ coordinate update of integration tests with team - - __cdk-init template change?:__ coordinated update of integration tests with team -* [ ] Docs - - __jsdocs__: All public APIs documented - - __README__: README and/or documentation topic updated - - __Design__: For significant features, design document added to `design` folder -* [ ] Title and Description - - __Change type__: title prefixed with **fix**, **feat** and module name in parens, which will appear in changelog - - __Title__: use lower-case and doesn't end with a period - - __Breaking?__: last paragraph: "BREAKING CHANGE: " - - __Issues__: Indicate issues fixed via: "**Fixes #xxx**" or "**Closes #xxx**" -* [ ] Sensitive Modules (requires 2 PR approvers) - - IAM Policy Document (in @aws-cdk/aws-iam) - - EC2 Security Groups and ACLs (in @aws-cdk/aws-ec2) - - Grant APIs (only if not based on official documentation with a reference) - ---- - -### Step 1: Open Issue - -If there isn't one already, open an issue describing what you intend to contribute. It's useful to communicate in -advance because if someone is already working in this space, it may be worth collaborating with them -instead of duplicating the effort. +Build the docker image: -### Step 2: Design (optional) +```console +$ docker build -t aws-cdk . +``` -In some cases, it is useful to seek feedback by iterating on a design document. This is useful -when you plan a big change or feature, or you want advice on what would be the best path forward. +This allows you to run the CDK in a CDK-compatible directory with a command like: -Sometimes, the GitHub issue is sufficient for such discussions, and can be sufficient to get -clarity on what you plan to do. Sometimes, a design document would work better, so people can provide -iterative feedback. +```console +$ docker run -v $(pwd):/app -w /app aws-cdk +``` -Before starting on a design, read through the [design guidelines](DESIGN_GUIDELINES.md) for general -patterns and tips. +## Gitpod (Alternative) -In such cases, use the GitHub issue description to collect **requirements** and -**use cases** for your feature. +You may also set up your local development environment using [Gitpod](http://gitpod.io) - +a service that allows you to spin up an in-browser Visual Studio Code-compatible editor, +with everything set up and ready to go for CDK development. +Just click the button below to create your private workspace: + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/aws/aws-cdk) + +This will start a new Gitpod workspace, with the CDK repository [pre-built](https://www.gitpod.io/docs/prebuilds/). +You can now work on your CDK repository, as described in the [Getting Started](#getting-started) section. -Then, create a design document in markdown format under the `design/` directory -and request feedback through a pull request. Prefix the PR title with "**RFC:**" -(request for comments). +Gitpod is free for 50 hours per month - make sure to stop your workspace when you're done +(you can always resume it later, and it won't need to run the build again). -Once the design is finalized, you can re-purpose this PR for the implementation, -or open a new PR to that end. +For Gitpod users only! The best way to supply CDK with your AWS credentials is to add them as +[persisting environment variables](https://www.gitpod.io/docs/environment-variables). +Adding them works as follows via terminal: + +```shell +eval $(gp env -e AWS_ACCESS_KEY_ID=XXXXXXXXX) +eval $(gp env -e AWS_SECRET_ACCESS_KEY=YYYYYYY) +eval $(gp env -e AWS_DEFAULT_REGION=ZZZZZZZZ) +eval $(gp env -e) +``` + +## Pull Requests + +### Step 1: Find something to work on + +If you want to contribute a specific feature or fix you have in mind, look at active [pull +requests](https://github.com/aws/aws-cdk/pulls) to see if someone else is already working on it. If not, you can start +contributing your changes. + +On the other hand, if you are here looking for an issue to work on, explore our [backlog of +issues](https://github.com/aws/aws-cdk/issues) and find something that piques your interest. We have labeled all of our +issues for easy filtration. +If you are looking for your first contribution, the ['good first issue' +label](https://github.com/aws/aws-cdk/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) will be of help. + +### Step 2: Design (optional) + +In some cases, it is useful to seek feedback by iterating on a design document. This is useful +when you plan a big change or feature, or you want advice on what would be the best path forward. + +In most cases, the GitHub issue is sufficient for such discussions, and can be sufficient to get +clarity on what you plan to do. If the changes are significant or intrusive to the existing CDK experience, +consider writing an RFC in our [RFC repository](https://github.com/aws/aws-cdk-rfcs) before jumping into our code base. ### Step 3: Work your Magic Work your magic. Here are some guidelines: -* Coding style (abbreviated): - * In general, follow the style of the code around you - * 2 space indentation - * 120 characters wide - * ATX style headings in markdown (e.g. `## H2 heading`) +* Coding style. + * If your change introduces a new construct, take a look at our [design guidelines](./docs/DESIGN_GUIDELINES.md) for + construct libraries. + We also have an [example construct library](packages/@aws-cdk/example-construct-library) that showcases a simple + construct library with a single construct. + * We have a number of linters that run during standard build that will enforce coding consistency and correctness. + Watch out for their error messages and adjust your code accordingly. * Every change requires a unit test * If you change APIs, make sure to update the module's README file * Try to maintain a single feature/bugfix per pull request. It's okay to introduce a little bit of housekeeping - changes along the way, but try to avoid conflating multiple features. Eventually, all these are going to go into a - single commit, so you can use that to frame your scope. -* If your change introduces a new construct, take a look at the our - [example Construct Library](packages/@aws-cdk/example-construct-library) for an explanation of the common patterns we use. - Feel free to start your contribution by copy&pasting files from that project, - and then edit and rename them as appropriate - - it might be easier to get started that way. -* If your change includes code examples (in the `README.md` file or as part of regular TSDoc tags), - you should probably validate those examples can be successfully compiled and trans-literated by - running `yarn rosetta:extract` (this requires other packages used by code examples are built). + changes along the way, but try to avoid conflating multiple features. Eventually, all these are going to go into a + single commit, so you can use that to frame your scope. #### Integration Tests @@ -189,17 +226,6 @@ Integration tests perform a few functions in the CDK code base - 3. (Optionally) Acts as a way to validate that constructs set up the CloudFormation resources as expected. A successful CloudFormation deployment does not mean that the resources are set up correctly. -For Gitpod users only! The best way to supply CDK with your AWS credentials is to add them as -[persisting environment variables](https://www.gitpod.io/docs/environment-variables). -Adding them works as follows via terminal: - -```shell -eval $(gp env -e AWS_ACCESS_KEY_ID=XXXXXXXXX) -eval $(gp env -e AWS_SECRET_ACCESS_KEY=YYYYYYY) -eval $(gp env -e AWS_DEFAULT_REGION=ZZZZZZZZ) -eval $(gp env -e) -``` - If you are working on a new feature that is using previously unused CloudFormation resource types, or involves configuring resource types across services, you need to write integration tests that use these resource types or features. @@ -220,6 +246,20 @@ Examples: * [integ.destinations.ts](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-lambda-destinations/test/integ.destinations.ts#L7) * [integ.token-authorizer.ts](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-apigateway/test/authorizers/integ.token-authorizer.ts#L6) +#### yarn watch (Optional) + +We've added a watch feature to the CDK that builds your code as you type it. Start this by running `yarn watch` for +each module that you are modifying. + +For example, watch the EC2 and IAM modules in a second terminal session: + +```console +$ cd packages/@aws-cdk/aws-ec2 +$ yarn watch & # runs in the background +$ cd packages/@aws-cdk/aws-iam +$ yarn watch & # runs in the background +``` + ### Step 4: Commit Create a commit with the proposed changes: @@ -250,21 +290,19 @@ BREAKING CHANGE: Description of what broke and how to achieve this behavior now ### Step 5: Pull Request -* Push to a GitHub fork or to a branch (naming convention: `/`). +* Push to a GitHub fork. + * CDK core members can push to a branch on the AWS CDK repo (naming convention: `/`). * Submit a Pull Request on GitHub. A reviewer will later be assigned by the maintainers. -* Please follow the PR checklist written below. We trust our contributors to self-check, and this helps that process! -* Discuss review comments and iterate until you get at least one “Approve”. When iterating, push new commits to the +* Discuss review comments and iterate until you get at least one "Approve". When iterating, push new commits to the same branch. Usually all these are going to be squashed when you merge to master. The commit messages should be hints for you when you finalize your merge commit message. * Make sure to update the PR title/description if things change. The PR title/description are going to be used as the commit title/message and will appear in the CHANGELOG, so maintain them all the way throughout the process. - - ### Step 6: Merge * Make sure your PR builds successfully (we have CodeBuild setup to automatically build all PRs). -* Once approved and tested, a maintainer will squash-merge to master and will use your PR title/description as the +* Once approved and tested, one of our bots will squash-merge to master and will use your PR title/description as the commit message. ## Breaking Changes @@ -275,13 +313,9 @@ programs that customers could have been writing against the current version of the CDK, that will no longer "work correctly" with the proposed new version of the CDK. -Breaking changes are not allowed in *stable* libraries¹. They are permissible -but still *highly discouraged* in experimental libraries, and require explicit -callouts in the bodies of Pull Requests that introduce them. - -> ¹) Note that starting in version 2 of the CDK, the majority of library code will be -> bundled into a single main CDK library which will be considered stable, and so -> no code in there can undergo breaking changes. +Breaking changes are not allowed in *stable* libraries. They are permitted +in experimental libraries, unless the maintainer of the module decides that it should be avoided. +Breaking changes require explicit callouts in the bodies of Pull Requests that introduce them. Breaking changes come in two flavors: @@ -327,8 +361,11 @@ $ yarn build $ yarn compat ``` -To figure out if the changes you made were breaking. See the section [API Compatibility -Checks](#api-compatibility-checks) for more information. +The only case where it is legitimate to break a public API is if the existing +API is a bug that blocked the usage of a feature. This means that by breaking +this API we will not break anyone, because they weren't able to use it. The file +`allowed-breaking-changes.txt` in the root of the repo is an exclusion file that +can be used in these cases. #### Dealing with breaking API surface changes @@ -404,57 +441,134 @@ If the new behavior is going to be breaking, the user must opt in to it, either Of these two, the first one is preferred if possible (as feature flags have non-local effects which can cause unintended effects). -## Tools +## Documentation + +Every module's README is rendered as the landing page of the official documentation. For example, this is +the README for the `aws-ec2` module - https://docs.aws.amazon.com/cdk/api/latest/docs/aws-ec2-readme.html. + +### Rosetta + +The README file contains code snippets written as typescript code. Code snippets typed in fenced code blocks +(such as `` ```ts ``) will be automatically extracted, compiled and translated to other languages when the +during the [pack](#pack) step. We call this feature 'rosetta'. -The CDK is a big project, and at the moment, all of the CDK modules are mastered in a single monolithic repository -(uses [lerna](https://github.com/lerna/lerna)). There are pros and cons to this approach, and it's especially valuable -to maintain integrity in the early stage of the project where things constantly change across the stack. In the future, -we believe many of these modules will be extracted to their own repositories. +You can run rosetta on the EC2 module (or any other module) by running: -Another complexity is that the CDK is packaged using [jsii](https://github.com/aws/jsii) to multiple programming -languages. This means that when a full build is complete, there will be a version of each module for each supported -language. +```console +$ cd packages/@aws-cdk/aws-ec2 +$ yarn rosetta:extract --strict +``` -However, in many cases, you can probably get away with just building a portion of the project, based on areas that you -want to work on. +To successfully do that, they must be compilable. The easiest way to do that is using +a *fixture*, which looks like this: -We recommend that you use [Visual Studio Code](https://code.visualstudio.com/) to work on the CDK. Be sure to install -the [eslint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) for it as well, since we have -strict linting rules that will prevent your code from compiling, but with VSCode and this extension can be automatically -fixed for you by hitting `Ctrl-.` when your cursor is on a red underline. +```` +```ts fixture=with-bucket +bucket.addLifecycleTransition({ ...props }); +``` +```` -### Main build scripts +While processing the examples, the tool will look for a file called +`rosetta/with-bucket.ts-fixture` in the package directory. This file will be +treated as a regular TypeScript source file, but it must also contain the text +`/// here`, at which point the example will be inserted. The complete file must +compile properly. -The build process is divided into stages, so you can invoke them as needed from the root of the repo: +Before the `/// here` marker, the fixture should import the necessary packages +and initialize the required variables. -- __`yarn build`__: runs the `build` and `test` commands in all modules (in topological order). -- __`yarn pack`__: packages all modules to all supported languages and produces a `dist/` directory with all the outputs - (running this script requires that you installed the [toolchains](#getting-started) for all target languages on your - system). +When no fixture is specified, the fixture with the name +`rosetta/default.ts-fixture` will be used if present. `nofixture` can be used to +opt out of that behavior. -### Partial build tools +In an `@example` block, which is unfenced, the first line of the example can +contain three slashes to achieve the same effect: -There are also two useful scripts in the `scripts` directory that can help you build part of the repo: +``` +/** + * @example + * /// fixture=with-bucket + * bucket.addLifecycleTransition({ ...props }); + */ +``` -- __`scripts/buildup`__: builds the current module and all of its dependencies (in topological order). -- __`scripts/builddown`__: builds the current module and all of its consumers (in topological order). +For a practical example of how making sample code compilable works, see the +`aws-ec2` package. -### Useful aliases +#### Recommendations -You can also add a few useful aliases to your shell profile: +In order to offer a consistent documentation style throughout the AWS CDK +codebase, example code should follow the following recommendations (there may be +cases where some of those do not apply - good judgement is to be applied): -```bash -# runs an npm script via lerna for a the current module -alias lr='lerna run --stream --scope $(node -p "require(\"./package.json\").name")' +- Types from the documented module should be **un-qualified**: -# runs "yarn build" (build + test) for the current module -alias lb='lr build' -alias lt='lr test' + ```ts + // An example in the @aws-cdk/core library, which defines Duration + Duration.minutes(15); + ``` + +- Types from other modules should be **qualified**: -# runs "yarn watch" for the current module (recommended to run in a separate terminal session): -alias lw='lr watch' + ```ts + // An example in the @aws-cdk/core library, using something from @aws-cdk/aws-s3 + const bucket = new s3.Bucket(this, 'Bucket'); + // ...rest of the example... + ``` + +- Within `.ts-fixture` files, make use of `declare` statements instead of + writing a compatible value (this will make your fixtures more durable): + + ```ts + // An hypothetical 'rosetta/default.ts-fixture' file in `@aws-cdk/core` + import * as kms from '@aws-cdk/aws-kms'; + import * as s3 from '@aws-cdk/aws-s3'; + import { StackProps } from '@aws-cdk/core'; + + declare const kmsKey: kms.IKey; + declare const bucket: s3.Bucket; + + declare const props: StackProps; + ``` + +## Tools (Advanced) + +### scripts/foreach.sh + +This wonderful tool allows you to execute a command for all modules in this repo +in topological order, but has the incredible property of being stateful. This +means that if a command fails, you can fix the issue and resume from where you +left off. + +To start a session, run: + +```console +$ scripts/foreach.sh COMMAND +``` + +This will execute "COMMAND" for each module in the repo (cwd will be the directory of the module). +If a task fails, it will stop. To resume, simply run `foreach.sh` again (with or without the same command). + +To reset the session (either when all tasks finished or if you wish to run a different session), run: + +```console +$ scripts/foreach.sh --reset +``` + +If you wish to run a command only against a module's dependency closure, use: + +```console +$ cd packages/my-module +$ ../scripts/foreach.sh --up COMMAND ``` +This will execute `COMMAND` against `my-module` and all its deps (in a topological order, of course). + +Consequently, there are two useful scripts that are built on top of `foreach.sh`, and lets you build modules. + +- __`scripts/buildup`__: builds the current module and all of its dependencies (in topological order). +- __`scripts/builddown`__: builds the current module and all of its consumers (in topological order). + ### Linters All linters are executed automatically as part of the build script, `yarn build`. @@ -501,14 +615,14 @@ $ lr pkglint **awslint** is a linter for the AWS Construct Library APIs. It is executed as a part of the build of all AWS modules in the project and enforces the [AWS -Construct Library Design Guidelines](./DESIGN_GUIDELINES.md). +Construct Library Design Guidelines](./docs/DESIGN_GUIDELINES.md). For more information about this tool, see the [awslint README](./packages/awslint/README.md). Generally speaking, if you make any changes which violate an awslint rule, build will fail with appropriate messages. All rules are documented and explained in -the [guidelines](./DESIGN_GUIDELINES.md). +the [guidelines](./docs/DESIGN_GUIDELINES.md). Here are a few useful commands: @@ -520,31 +634,6 @@ Here are a few useful commands: evaluate only the rule specified [awslint README](./packages/awslint/README.md) for details on include/exclude rule patterns. - -#### jsii-rosetta - -**jsii-rosetta** can be used to verify that all code examples included in documentation for a package (including those -in `README.md`) successfully compile against the library they document. It is recommended to run it to ensure all -examples are still accurate. Successfully building examples is also necessary to ensure the best possible translation to -other supported languages (`C#`, `Java`, `Python`, ...). - -> Note that examples may use libraries that are not part of the `dependencies` or `devDependencies` of the documented -> package. For example, `@aws-cdk/core` contains many examples that leverage libraries built *on top of it* (such as -> `@aws-cdk/aws-sns`). Such libraries must be built (using `yarn build`) before **jsii-rosetta** can verify that -> examples are correct. - -To run **jsii-rosetta** in *strict* mode (so that it always fails when encountering examples that fail to compile), use -the following command: - -```console -$ yarn rosetta:extract --strict -``` - -For more information on how you can address examples that fail compiling due to missing fixtures (declarations that are -necessary for the example to compile, but which would distract the reader away from what is being demonstrated), you -might need to introduce [rosetta fixtures](https://github.com/aws/jsii/tree/main/packages/jsii-rosetta#fixtures). Refer -to the [Examples](#examples) section. - ### cfn2ts This tool is used to generate our low-level CloudFormation resources @@ -561,37 +650,6 @@ Each module also has an npm script called `cfn2ts`: * `yarn cfn2ts`: generates L1 for a specific module * `lerna run cfn2ts`: generates L1 for the entire repo -### scripts/foreach.sh - -This wonderful tool allows you to execute a command for all modules in this repo -in topological order, but has the incredible property of being stateful. This -means that if a command fails, you can fix the issue and resume from where you -left off. - -To start a session, run: - -```console -$ scripts/foreach.sh COMMAND -``` - -This will execute "COMMAND" for each module in the repo (cwd will be the directory of the module). -If a task fails, it will stop. To resume, simply run `foreach.sh` again (with or without the same command). - -To reset the session (either when all tasks finished or if you wish to run a different session), run: - -```console -$ scripts/foreach.sh --reset -``` - -If you wish to run a command only against a module's dependency closure, use: - -```console -$ cd packages/my-module -$ ../scripts/foreach.sh --up COMMAND -``` - -This will execute `COMMAND` against `my-module` and all its deps (in a topological order, of course). - ### Jetbrains support (WebStorm/IntelliJ) This project uses lerna and utilizes symlinks inside nested `node_modules` directories. You may encounter an issue during @@ -599,146 +657,19 @@ indexing where the IDE attempts to index these directories and keeps following l available memory and crashes. To fix this, you can run ```node ./scripts/jetbrains-remove-node-modules.js``` to exclude these directories. -## Workflows - -This section includes step-by-step descriptions of common workflows. - -### Full clean build - -Clone the repo: - -```console -$ git clone https://github.com/aws/aws-cdk.git -$ cd aws-cdk -``` - -If you already have a local repo and you want a fresh build, run `git clean -fdx` from the root. - -Install and build: - -```console -$ ./install.sh -$ yarn build -``` - -If you also wish to package to all languages, make sure you have all the [toolchains](#getting-started) and now run: - -``` -$ ./pack.sh -``` - -> NOTE: in local builds, pack.sh will finish but will fail with an error -> indicating the build artifacts use the marker version (`0.0.0`). This is -> normal, and you can trust the output in `dist/` despite the failure. This is a -> protection we have to make sure we don't accidentally release artifacts with -> the marker version. - -### Full Docker build - -Clone the repo: - -```console -$ git clone https://github.com/aws/aws-cdk.git -$ cd aws-cdk -``` - -If you already have a local repo and you want a fresh build, run `git clean -fdx` from the root. - -Build the docker image: - -```console -$ docker build -t aws-cdk . -``` - -This allows you to run the CDK in a CDK-compatible directory with a command like: - -```console -$ docker run -v $(pwd):/app -w /app aws-cdk -``` - -### Partial build - -In many cases, you don't really need to build the entire project. Say you want to work on the `@aws-cdk/aws-ec2` module: - -```console -$ yarn install -$ cd packages/@aws-cdk/aws-ec2 -$ ../../../scripts/buildup -``` - -Note that `buildup` uses `foreach.sh`, which means it is resumable. If your build fails and you wish to resume, just run -`buildup --resume`. If you wish to restart, run `buildup` again. - -### Partial pack - -Packing involves generating CDK code in the various target languages and packaging them up to be published to their -respective package managers. Once in a while, these will need to be generated either to test the experience of a new -feature, or reproduce a packaging failure. - -Before running this, make sure either that the CDK module and all of its dependencies are already built. See [Partial -build](#partial-build) or [Full clean build](#full-clean-build). - -To package a specific module, say the `@aws-cdk/aws-ec2` module: - -```console -$ cd -$ docker run --rm --net=host -it -v $PWD:$PWD -w $PWD jsii/superchain -docker$ cd packages/@aws-cdk/aws-ec2 -docker$ ../../../scripts/foreach.sh --up yarn run package -docker$ exit -``` - -The `dist/` folder within each module contains the packaged up language artifacts. - -### Quick Iteration - -After you've built the modules you want to work on once, use `yarn watch` for each module that you are modifying. - -Watch the EC2 and IAM modules in a second terminal session: - -```console -$ cd packages/@aws-cdk/aws-ec2 -$ yarn watch & # runs in the background -$ cd packages/@aws-cdk/aws-iam -$ yarn watch & # runs in the background -``` - -Code... - -Now to test, you can either use `yarn test` or invoke nodeunit/jest directly: - -Running nodeunit tests directly on a module: -```console -$ cd packages/@aws-cdk/aws-iam -$ nodeunit test/test.*.js - -``` - -Running jest tests directly on a module: -```console -$ cd packages/@aws-cdk/aws-iam -$ jest test/*test.js - -``` - ### Linking against this repository -The script `./link-all.sh` can be used to generate symlinks to all modules in this repository under some `node_module` -directory. This can be used to develop against this repo as a local dependency. +If you are developing your own CDK application or library and want to use the locally checked out version of the +AWS CDK, instead of the the version of npm, the `./link-all.sh` script will help here. -One can use the `postinstall` script to symlink this repo: +This script symlinks the built modules from the local AWS CDK repo under the `node_modules/` folder of the CDK app or +library. -```json -{ - "scripts": { - "postinstall": "../aws-cdk/link-all.sh" - } -} +```console +$ cd +$ /link-all.sh ``` -This assumes this repo is a sibling of the target repo and will install the CDK as a linked dependency during -`yarn install`. - ### Running integration tests in parallel Integration tests may take a long time to complete. We can speed this up by running them in parallel @@ -760,23 +691,7 @@ Use GraphViz with `template-deps-to-dot`: $ cdk -a some.app.js synth | $awscdk/scripts/template-deps-to-dot | dot -Tpng > deps.png ``` -### Adding Dependencies - -The root [package.json](./package.json) includes global devDependencies (see -[lerna docs](https://github.com/lerna/lerna#common-devdependencies)) on the topic. - - * To add a global dependency, run `yarn add --dev` at the root. - * To add a dependency for a specific module, run `yarn add ` inside the module's directory. - -Guidelines: - - * We cannot accept dependencies that use non-permissive open source licenses (Apache, MIT, etc). - * Make sure dependencies are defined using [caret - ranges](https://docs.npmjs.com/misc/semver#caret-ranges-123-025-004) (e.g. `^1.2.3`). This enables non-breaking - updates to automatically be picked up. - * Make sure `yarn.lock` is included in your commit. - -### Finding dependency cycles between packages +### Find dependency cycles between packages You can use `find-cycles` to print a list of internal dependency cycles: @@ -792,159 +707,19 @@ Cycle: @aws-cdk/aws-sns => @aws-cdk/aws-lambda => @aws-cdk/aws-codecommit => @aw Cycle: @aws-cdk/aws-sns => @aws-cdk/aws-lambda => @aws-cdk/aws-codecommit => @aws-cdk/aws-codepipeline => @aws-cdk/aws-sns ``` -### Updating all Dependencies - -To update all dependencies (without bumping major versions): - -1. Obtain a fresh clone from "master". -2. Run `yarn install` -3. Run `./scripts/update-dependencies.sh --mode full` (use `--mode semver` to avoid bumping major versions) -4. Submit a Pull Request. - -### Running CLI integration tests +## Running CLI integration tests The CLI package (`packages/aws-cdk`) has some integration tests that aren't run as part of the regular build, since they have some particular requirements. See the [CLI CONTRIBUTING.md file](packages/aws-cdk/CONTRIBUTING.md) for more information on running those tests. -### Changing Cloud Assembly Schema +## Changing Cloud Assembly Schema If you plan on making changes to the `cloud-assembly-schema` package, make sure you familiarize yourself with its own [contribution guide](./packages/@aws-cdk/cloud-assembly-schema/CONTRIBUTING.md) -### API Compatibility Checks - -All stable APIs in the CDK go through a compatibility check during build using -the [jsii-diff] tool. This tool downloads the latest released version from npm -and verifies that the APIs in the current build have not changed in a breaking -way. - -[jsii-diff]: https://www.npmjs.com/package/jsii-diff - -Compatibility checks always run as part of a full build (`yarn build`). - -You can use `yarn compat` to run compatibility checks for all modules: - -```shell -(working directory is repo root) -$ yarn build -$ yarn compat -``` - -You can also run `compat` from individual package directories: - -```shell -$ cd packages/@aws-cdk/aws-sns -$ yarn build -$ yarn compat -``` - -The only case where it is legitimate to break a public API is if the existing -API is a bug that blocked the usage of a feature. This means that by breaking -this API we will not break anyone, because they weren't able to use it. The file -`allowed-breaking-changes.txt` in the root of the repo is an exclusion file that -can be used in these cases. - -### Examples - -#### Fixture Files - -Examples typed in fenced code blocks (looking like `'''ts`, but then with backticks -instead of regular quotes) will be automatically extracted, compiled and translated -to other languages when the bindings are generated. - -To successfully do that, they must be compilable. The easiest way to do that is using -a *fixture*, which looks like this: - -``` -'''ts fixture=with-bucket -bucket.addLifecycleTransition({ ...props }); -''' -``` - -While processing the examples, the tool will look for a file called -`rosetta/with-bucket.ts-fixture` in the package directory. This file will be -treated as a regular TypeScript source file, but it must also contain the text -`/// here`, at which point the example will be inserted. The complete file must -compile properly. - -Before the `/// here` marker, the fixture should import the necessary packages -and initialize the required variables. - -If no fixture is specified, the fixture with the name -`rosetta/default.ts-fixture` will be used if present. `nofixture` can be used to -opt out of that behavior. - -In an `@example` block, which is unfenced, the first line of the example can -contain three slashes to achieve the same effect: - -``` -/** - * @example - * /// fixture=with-bucket - * bucket.addLifecycleTransition({ ...props }); - */ -``` - -When including packages in your examples (even the package you're writing the -examples for), use the full package name (e.g. `import s3 = -require('@aws-cdk/aws-s3);`). The example will be compiled in an environment -where all CDK packages are available using their public names. In this way, -it's also possible to import packages that are not in the dependency set of -the current package. - -For a practical example of how making sample code compilable works, see the -`aws-ec2` package. - -#### Recommendations - -In order to offer a consistent documentation style throughout the AWS CDK -codebase, example code should follow the following recommendations (there may be -cases where some of those do not apply - good judgement is to be applied): - -- Types from the documented module should be **un-qualified**: - - ```ts - // An example in the @aws-cdk/core library, which defines Duration - Duration.minutes(15); - ``` - -- Types from other modules should be **qualified**: - - ```ts - // An example in the @aws-cdk/core library, using something from @aws-cdk/aws-s3 - const bucket = new s3.Bucket(this, 'Bucket'); - // ...rest of the example... - ``` - -- Within `.ts-fixture` files, make use of `declare` statements instead of - writing a compatible value (this will make your fixtures more durable): - - ```ts - // An hypothetical 'rosetta/default.ts-fixture' file in `@aws-cdk/core` - import * as kms from '@aws-cdk/aws-kms'; - import * as s3 from '@aws-cdk/aws-s3'; - import { StackProps } from '@aws-cdk/core'; - - declare const kmsKey: kms.IKey; - declare const bucket: s3.Bucket; - - declare const props: StackProps; - ``` - -> Those recommendations are not verified or enforced by automated tooling. Pull -> request reviewers may however request that new sample code is edited to meet -> those requirements as needed. - -#### Checking a single package - -Examples of all packages are extracted and compiled as part of the packaging -step. If you are working on getting rid of example compilation errors of a -single package, you can run `yarn rosetta:extract --strict` in the package's -directory (see the [**jsii-rosetta**](#jsii-rosetta) section). - -### Feature Flags +## Feature Flags Sometimes we want to introduce new breaking behavior because we believe this is the correct default behavior for the CDK. The problem of course is that breaking @@ -987,7 +762,7 @@ CDK](https://github.com/aws/aws-cdk/issues/3398) we will either remove the legacy behavior or flip the logic for all these features and then reset the `FEATURE_FLAGS` map for the next cycle. -#### CDKv2 +### Feature Flags - CDKv2 We have started working on the next version of the CDK, specifically CDKv2. This is currently being maintained on a separate branch `v2-main` whereas `master` continues to track versions `1.x`. @@ -1003,59 +778,10 @@ behaviour when flags are enabled or disabled in the two major versions. [jest helper methods]: https://github.com/aws/aws-cdk/blob/master/tools/cdk-build-tools/lib/feature-flag.ts -### Versioning and Release - -The `release.json` file at the root of the repo determines which release line -this branch belongs to. - -```js -{ - "majorVersion": 1 | 2, - "releaseType": "stable" | "alpha" | "rc" -} -``` - -To reduce merge conflicts in automatic merges between version branches, the -current version number is stored under `version.vNN.json` (where `NN` is -`majorVersion`) and changelogs are stored under `CHANGELOG.NN.md` (for -historical reasons, the changelog for 1.x is under `CHANGELOG.md`). When we -fork to a new release branch (e.g. `v2-main`), we will update `release.json` in -this branch to reflect the new version line, and this information will be used -to determine how releases are cut. - -The actual `version` field in all `package.json` files should always be `0.0.0`. -This means that local development builds will use version `0.0.0` instead of the -official version from the version file. - -#### `./bump.sh` - -This script uses [standard-version] to update the version in `version.vNN.json` -to the next version. By default it will perform a **minor** bump, but `./bump.sh -patch` can be used to perform a patch release if that's needed. - -This script will also update the relevant changelog file. - -[standard-version]: https://github.com/conventional-changelog/standard-version - -#### `scripts/resolve-version.js` - -The script evaluates evaluates the configuration in `release.json` and exports an -object like this: - -```js -{ - version: '2.0.0-alpha.1', // the current version - versionFile: 'version.v2.json', // the version file - changelogFile: 'CHANGELOG.v2.md', // changelog file name - prerelease: 'alpha', // prerelease tag (undefined for stable) - marker: '0.0.0' // version marker in package.json files -} -``` - -#### scripts/align-version.sh +## Versioning and Release -In official builds, the `scripts/align-version.sh` is used to update all -`package.json` files based on the version from `version.vNN.json`. +See [release.md](./docs/release.md) for details on how CDK versions are maintained and how +to trigger a new release ## Troubleshooting diff --git a/DESIGN_GUIDELINES.md b/docs/DESIGN_GUIDELINES.md similarity index 100% rename from DESIGN_GUIDELINES.md rename to docs/DESIGN_GUIDELINES.md diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000000000..f7f5f9612ea23 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,53 @@ +# Versioning and Release + +The `release.json` file at the root of the repo determines which release line +this branch belongs to. + +```js +{ + "majorVersion": 1 | 2, + "releaseType": "stable" | "alpha" | "rc" +} +``` + +To reduce merge conflicts in automatic merges between version branches, the +current version number is stored under `version.vNN.json` (where `NN` is +`majorVersion`) and changelogs are stored under `CHANGELOG.NN.md` (for +historical reasons, the changelog for 1.x is under `CHANGELOG.md`). When we +fork to a new release branch (e.g. `v2-main`), we will update `release.json` in +this branch to reflect the new version line, and this information will be used +to determine how releases are cut. + +The actual `version` field in all `package.json` files should always be `0.0.0`. +This means that local development builds will use version `0.0.0` instead of the +official version from the version file. + +## `./bump.sh` + +This script uses [standard-version] to update the version in `version.vNN.json` +to the next version. By default it will perform a **minor** bump, but `./bump.sh +patch` can be used to perform a patch release if that's needed. + +This script will also update the relevant changelog file. + +[standard-version]: https://github.com/conventional-changelog/standard-version + +## `scripts/resolve-version.js` + +The script evaluates evaluates the configuration in `release.json` and exports an +object like this: + +```js +{ + version: '2.0.0-alpha.1', // the current version + versionFile: 'version.v2.json', // the version file + changelogFile: 'CHANGELOG.v2.md', // changelog file name + prerelease: 'alpha', // prerelease tag (undefined for stable) + marker: '0.0.0' // version marker in package.json files +} +``` + +## scripts/align-version.sh + +In official builds, the `scripts/align-version.sh` is used to update all +`package.json` files based on the version from `version.vNN.json`. \ No newline at end of file From 99d83e9a0d08d3794122d7a455b677541011a200 Mon Sep 17 00:00:00 2001 From: Jan Brauer Date: Tue, 16 Mar 2021 16:39:38 +0100 Subject: [PATCH 23/32] feat(lambda-event-sources): msk and self-managed kafka event sources (#12507) Fixes #12099 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-lambda-event-sources/README.md | 61 ++++ .../aws-lambda-event-sources/lib/index.ts | 1 + .../aws-lambda-event-sources/lib/kafka.ts | 194 ++++++++++ .../aws-lambda-event-sources/lib/stream.ts | 2 +- .../aws-lambda-event-sources/package.json | 6 + .../test/test.kafka.ts | 334 ++++++++++++++++++ .../aws-lambda/lib/event-source-mapping.ts | 104 +++++- .../test/event-source-mapping.test.ts | 91 +++++ packages/@aws-cdk/aws-msk/README.md | 8 + packages/@aws-cdk/aws-msk/lib/cluster.ts | 34 ++ packages/@aws-cdk/aws-msk/lib/index.ts | 1 + packages/@aws-cdk/aws-msk/package.json | 2 +- 12 files changed, 835 insertions(+), 3 deletions(-) create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts create mode 100644 packages/@aws-cdk/aws-lambda-event-sources/test/test.kafka.ts create mode 100644 packages/@aws-cdk/aws-msk/lib/cluster.ts diff --git a/packages/@aws-cdk/aws-lambda-event-sources/README.md b/packages/@aws-cdk/aws-lambda-event-sources/README.md index 5e1578d9aefd4..eb4f206c8f3c9 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/README.md +++ b/packages/@aws-cdk/aws-lambda-event-sources/README.md @@ -207,6 +207,67 @@ myFunction.addEventSource(new KinesisEventSource(stream, { })); ``` +## Kafka + +You can write Lambda functions to process data either from [Amazon MSK](https://docs.aws.amazon.com/lambda/latest/dg/with-msk.html) or a [self managed Kafka](https://docs.aws.amazon.com/lambda/latest/dg/kafka-smaa.html) cluster. + +The following code sets up Amazon MSK as an event source for a lambda function. Credentials will need to be configured to access the +MSK cluster, as described in [Username/Password authentication](https://docs.aws.amazon.com/msk/latest/developerguide/msk-password.html). + +```ts +import * as lambda from '@aws-cdk/aws-lambda'; +import * as msk from '@aws-cdk/aws-lambda'; +import { Secret } from '@aws-cdk/aws-secretmanager'; +import { ManagedKafkaEventSource } from '@aws-cdk/aws-lambda-event-sources'; + +// Your MSK cluster +const cluster = msk.Cluster.fromClusterArn(this, 'Cluster', + 'arn:aws:kafka:us-east-1:0123456789019:cluster/SalesCluster/abcd1234-abcd-cafe-abab-9876543210ab-4'); + +// The Kafka topic you want to subscribe to +const topic = 'some-cool-topic' + +// The secret that allows access to your MSK cluster +// You still have to make sure that it is associated with your cluster as described in the documentation +const secret = new Secret(this, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + +myFunction.addEventSource(new ManagedKafkaEventSource({ + cluster: cluster, + topic: topic, + secret: secret, + batchSize: 100, // default + startingPosition: lambda.StartingPosition.TRIM_HORIZON +})); +``` + +The following code sets up a self managed Kafka cluster as an event source. Username and password based authentication +will need to be set up as described in [Managing access and permissions](https://docs.aws.amazon.com/lambda/latest/dg/smaa-permissions.html#smaa-permissions-add-secret). + +```ts +import * as lambda from '@aws-cdk/aws-lambda'; +import { Secret } from '@aws-cdk/aws-secretmanager'; +import { SelfManagedKafkaEventSource } from '@aws-cdk/aws-lambda-event-sources'; + +// The list of Kafka brokers +const bootstrapServers = ['kafka-broker:9092'] + +// The Kafka topic you want to subscribe to +const topic = 'some-cool-topic' + +// The secret that allows access to your self hosted Kafka cluster +const secret = new Secret(this, 'Secret', { ... }); + +myFunction.addEventSource(new SelfManagedKafkaEventSource({ + bootstrapServers: bootstrapServers, + topic: topic, + secret: secret, + batchSize: 100, // default + startingPosition: lambda.StartingPosition.TRIM_HORIZON +})); +``` + +If your self managed Kafka cluster is only reachable via VPC also configure `vpc` `vpcSubnets` and `securityGroup`. + ## Roadmap Eventually, this module will support all the event sources described under diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/index.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/index.ts index 19253a743cae8..555137e51afbf 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/index.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/index.ts @@ -1,5 +1,6 @@ export * from './api'; export * from './dynamodb'; +export * from './kafka'; export * from './kinesis'; export * from './s3'; export * from './sns'; diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts new file mode 100644 index 0000000000000..f0782964d93ce --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/kafka.ts @@ -0,0 +1,194 @@ +import * as crypto from 'crypto'; +import { ISecurityGroup, IVpc, SubnetSelection } from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as msk from '@aws-cdk/aws-msk'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; +import { Stack } from '@aws-cdk/core'; +import { StreamEventSource, StreamEventSourceProps } from './stream'; + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + +/** + * Properties for a Kafka event source + */ +export interface KafkaEventSourceProps extends StreamEventSourceProps { + /** + * the Kafka topic to subscribe to + */ + readonly topic: string, + /** + * the secret with the Kafka credentials, see https://docs.aws.amazon.com/msk/latest/developerguide/msk-password.html for details + */ + readonly secret: secretsmanager.ISecret +} + +/** + * Properties for a MSK event source + */ +export interface ManagedKafkaEventSourceProps extends KafkaEventSourceProps { + /** + * an MSK cluster construct + */ + readonly cluster: msk.ICluster +} + +/** + * The authentication method to use with SelfManagedKafkaEventSource + */ +export enum AuthenticationMethod { + /** + * SASL_SCRAM_512_AUTH authentication method for your Kafka cluster + */ + SASL_SCRAM_512_AUTH = 'SASL_SCRAM_512_AUTH', + /** + * SASL_SCRAM_256_AUTH authentication method for your Kafka cluster + */ + SASL_SCRAM_256_AUTH = 'SASL_SCRAM_256_AUTH', +} + +/** + * Properties for a self managed Kafka cluster event source. + * If your Kafka cluster is only reachable via VPC make sure to configure it. + */ +export interface SelfManagedKafkaEventSourceProps extends KafkaEventSourceProps { + /** + * The list of host and port pairs that are the addresses of the Kafka brokers in a "bootstrap" Kafka cluster that + * a Kafka client connects to initially to bootstrap itself. They are in the format `abc.xyz.com:xxxx`. + */ + readonly bootstrapServers: string[] + + /** + * If your Kafka brokers are only reachable via VPC provide the VPC here + * + * @default none + */ + readonly vpc?: IVpc; + + /** + * If your Kafka brokers are only reachable via VPC, provide the subnets selection here + * + * @default - none, required if setting vpc + */ + readonly vpcSubnets?: SubnetSelection, + + /** + * If your Kafka brokers are only reachable via VPC, provide the security group here + * + * @default - none, required if setting vpc + */ + readonly securityGroup?: ISecurityGroup + + /** + * The authentication method for your Kafka cluster + * + * @default AuthenticationMethod.SASL_SCRAM_512_AUTH + */ + readonly authenticationMethod?: AuthenticationMethod +} + +/** + * Use a MSK cluster as a streaming source for AWS Lambda + */ +export class ManagedKafkaEventSource extends StreamEventSource { + // This is to work around JSII inheritance problems + private innerProps: ManagedKafkaEventSourceProps; + + constructor(props: ManagedKafkaEventSourceProps) { + super(props); + this.innerProps = props; + } + + public bind(target: lambda.IFunction) { + target.addEventSourceMapping( + `KafkaEventSource:${this.innerProps.cluster.clusterArn}${this.innerProps.topic}`, + this.enrichMappingOptions({ + eventSourceArn: this.innerProps.cluster.clusterArn, + startingPosition: this.innerProps.startingPosition, + // From https://docs.aws.amazon.com/msk/latest/developerguide/msk-password.html#msk-password-limitations, "Amazon MSK only supports SCRAM-SHA-512 authentication." + sourceAccessConfigurations: [{ type: lambda.SourceAccessConfigurationType.SASL_SCRAM_512_AUTH, uri: this.innerProps.secret.secretArn }], + kafkaTopic: this.innerProps.topic, + }), + ); + + this.innerProps.secret.grantRead(target); + + target.addToRolePolicy(new iam.PolicyStatement( + { + actions: ['kafka:DescribeCluster', 'kafka:GetBootstrapBrokers', 'kafka:ListScramSecrets'], + resources: [this.innerProps.cluster.clusterArn], + }, + )); + + target.role?.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaMSKExecutionRole')); + } +} + +/** + * Use a self hosted Kafka installation as a streaming source for AWS Lambda. + */ +export class SelfManagedKafkaEventSource extends StreamEventSource { + // This is to work around JSII inheritance problems + private innerProps: SelfManagedKafkaEventSourceProps; + + constructor(props: SelfManagedKafkaEventSourceProps) { + super(props); + if (props.vpc) { + if (!props.securityGroup) { + throw new Error('securityGroup must be set when providing vpc'); + } + if (!props.vpcSubnets) { + throw new Error('vpcSubnets must be set when providing vpc'); + } + } + this.innerProps = props; + } + + public bind(target: lambda.IFunction) { + if (!Construct.isConstruct(target)) { throw new Error('Function is not a construct. Unexpected error.'); } + target.addEventSourceMapping( + this.mappingId(target), + this.enrichMappingOptions({ + kafkaBootstrapServers: this.innerProps.bootstrapServers, + kafkaTopic: this.innerProps.topic, + startingPosition: this.innerProps.startingPosition, + sourceAccessConfigurations: this.sourceAccessConfigurations(), + }), + ); + this.innerProps.secret.grantRead(target); + } + + private mappingId(target: lambda.IFunction) { + let hash = crypto.createHash('md5'); + hash.update(JSON.stringify(Stack.of(target).resolve(this.innerProps.bootstrapServers))); + const idHash = hash.digest('hex'); + return `KafkaEventSource:${idHash}:${this.innerProps.topic}`; + } + + private sourceAccessConfigurations() { + let authType; + switch (this.innerProps.authenticationMethod) { + case AuthenticationMethod.SASL_SCRAM_256_AUTH: + authType = lambda.SourceAccessConfigurationType.SASL_SCRAM_256_AUTH; + break; + case AuthenticationMethod.SASL_SCRAM_512_AUTH: + default: + authType = lambda.SourceAccessConfigurationType.SASL_SCRAM_512_AUTH; + break; + } + let sourceAccessConfigurations = [{ type: authType, uri: this.innerProps.secret.secretArn }]; + if (this.innerProps.vpcSubnets !== undefined && this.innerProps.securityGroup !== undefined) { + sourceAccessConfigurations.push({ + type: lambda.SourceAccessConfigurationType.VPC_SECURITY_GROUP, + uri: this.innerProps.securityGroup.securityGroupId, + }, + ); + this.innerProps.vpc?.selectSubnets(this.innerProps.vpcSubnets).subnetIds.forEach((id) => { + sourceAccessConfigurations.push({ type: lambda.SourceAccessConfigurationType.VPC_SUBNET, uri: id }); + }); + } + return sourceAccessConfigurations; + } +} diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/stream.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/stream.ts index d18eaaf3f947c..96907b97835fc 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/stream.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/stream.ts @@ -3,7 +3,7 @@ import { Duration } from '@aws-cdk/core'; /** * The set of properties for event sources that follow the streaming model, - * such as, Dynamo and Kinesis. + * such as, Dynamo, Kinesis and Kafka. */ export interface StreamEventSourceProps { /** diff --git a/packages/@aws-cdk/aws-lambda-event-sources/package.json b/packages/@aws-cdk/aws-lambda-event-sources/package.json index 20112e1029cbf..33906c1f5219e 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/package.json +++ b/packages/@aws-cdk/aws-lambda-event-sources/package.json @@ -72,12 +72,15 @@ "dependencies": { "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-msk": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-notifications": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", @@ -88,12 +91,15 @@ "peerDependencies": { "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-msk": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-s3-notifications": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sns-subscriptions": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-event-sources/test/test.kafka.ts b/packages/@aws-cdk/aws-lambda-event-sources/test/test.kafka.ts new file mode 100644 index 0000000000000..9cf1af9e50507 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-event-sources/test/test.kafka.ts @@ -0,0 +1,334 @@ +import { arrayWith, expect, haveResource } from '@aws-cdk/assert'; +import { SecurityGroup, SubnetType, Vpc } from '@aws-cdk/aws-ec2'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as msk from '@aws-cdk/aws-msk'; +import { Secret } from '@aws-cdk/aws-secretsmanager'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as sources from '../lib'; +import { TestFunction } from './test-function'; + +export = { + 'msk': { + 'default'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const cluster = msk.Cluster.fromClusterArn(stack, 'Cluster', 'some-arn'); + const kafkaTopic = 'some-topic'; + const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + + // WHEN + fn.addEventSource(new sources.ManagedKafkaEventSource( + { + cluster: cluster, + topic: kafkaTopic, + secret: secret, + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + })); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: { + Ref: 'SecretA720EF05', + }, + }, + { + Action: [ + 'kafka:DescribeCluster', + 'kafka:GetBootstrapBrokers', + 'kafka:ListScramSecrets', + ], + Effect: 'Allow', + Resource: cluster.clusterArn, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'FnServiceRoleDefaultPolicyC6A839BF', + Roles: [ + { + Ref: 'FnServiceRoleB9001A96', + }, + ], + })); + + expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + EventSourceArn: cluster.clusterArn, + FunctionName: { + Ref: 'Fn9270CBC0', + }, + BatchSize: 100, + StartingPosition: 'TRIM_HORIZON', + Topics: [ + kafkaTopic, + ], + SourceAccessConfigurations: [ + { + Type: 'SASL_SCRAM_512_AUTH', + URI: { + Ref: 'SecretA720EF05', + }, + }, + ], + })); + + test.done(); + }, + }, + + 'self managed kafka': { + 'default'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const kafkaTopic = 'some-topic'; + const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + const bootstrapServers = ['kafka-broker:9092']; + + // WHEN + fn.addEventSource(new sources.SelfManagedKafkaEventSource( + { + bootstrapServers: bootstrapServers, + topic: kafkaTopic, + secret: secret, + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + })); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: { + Ref: 'SecretA720EF05', + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'FnServiceRoleDefaultPolicyC6A839BF', + Roles: [ + { + Ref: 'FnServiceRoleB9001A96', + }, + ], + })); + + expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + FunctionName: { + Ref: 'Fn9270CBC0', + }, + BatchSize: 100, + SelfManagedEventSource: { + Endpoints: { + KafkaBootstrapServers: bootstrapServers, + }, + }, + StartingPosition: 'TRIM_HORIZON', + Topics: [ + kafkaTopic, + ], + SourceAccessConfigurations: [ + { + Type: 'SASL_SCRAM_512_AUTH', + URI: { + Ref: 'SecretA720EF05', + }, + }, + ], + })); + + test.done(); + }, + + VPC: { + 'correctly rendered in the stack'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const kafkaTopic = 'some-topic'; + const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + const bootstrapServers = ['kafka-broker:9092']; + const sg = SecurityGroup.fromSecurityGroupId(stack, 'SecurityGroup', 'sg-0123456789'); + const vpc = new Vpc(stack, 'Vpc'); + + // WHEN + fn.addEventSource(new sources.SelfManagedKafkaEventSource( + { + bootstrapServers: bootstrapServers, + topic: kafkaTopic, + secret: secret, + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + vpc: vpc, + vpcSubnets: { subnetType: SubnetType.PRIVATE }, + securityGroup: sg, + })); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: { + Ref: 'SecretA720EF05', + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'FnServiceRoleDefaultPolicyC6A839BF', + Roles: [ + { + Ref: 'FnServiceRoleB9001A96', + }, + ], + })); + + expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + FunctionName: { + Ref: 'Fn9270CBC0', + }, + BatchSize: 100, + SelfManagedEventSource: { + Endpoints: { + KafkaBootstrapServers: bootstrapServers, + }, + }, + StartingPosition: 'TRIM_HORIZON', + Topics: [ + kafkaTopic, + ], + SourceAccessConfigurations: [ + { + Type: 'SASL_SCRAM_512_AUTH', + URI: { + Ref: 'SecretA720EF05', + }, + }, + { + Type: 'VPC_SECURITY_GROUP', + URI: 'sg-0123456789', + }, + { + Type: 'VPC_SUBNET', + URI: { + Ref: 'VpcPrivateSubnet1Subnet536B997A', + }, + }, + { + Type: 'VPC_SUBNET', + URI: { + Ref: 'VpcPrivateSubnet2Subnet3788AAA1', + }, + }, + ], + })); + + test.done(); + }, + 'setting vpc requires vpcSubnets to be set'(test: Test) { + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const kafkaTopic = 'some-topic'; + const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + const bootstrapServers = ['kafka-broker:9092']; + const vpc = new Vpc(stack, 'Vpc'); + + test.throws(() => { + fn.addEventSource(new sources.SelfManagedKafkaEventSource( + { + bootstrapServers: bootstrapServers, + topic: kafkaTopic, + secret: secret, + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + vpc: vpc, + securityGroup: SecurityGroup.fromSecurityGroupId(stack, 'SecurityGroup', 'sg-0123456789'), + + })); + }, /vpcSubnets must be set/); + + test.done(); + }, + + 'setting vpc requires securityGroup to be set'(test: Test) { + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const kafkaTopic = 'some-topic'; + const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + const bootstrapServers = ['kafka-broker:9092']; + const vpc = new Vpc(stack, 'Vpc'); + + test.throws(() => { + fn.addEventSource(new sources.SelfManagedKafkaEventSource( + { + bootstrapServers: bootstrapServers, + topic: kafkaTopic, + secret: secret, + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + vpc: vpc, + vpcSubnets: { subnetType: SubnetType.PRIVATE }, + })); + }, /securityGroup must be set/); + + test.done(); + }, + }, + + 'using SCRAM-SHA-256'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new TestFunction(stack, 'Fn'); + const kafkaTopic = 'some-topic'; + const secret = new Secret(stack, 'Secret', { secretName: 'AmazonMSK_KafkaSecret' }); + const bootstrapServers = ['kafka-broker:9092']; + const sg = SecurityGroup.fromSecurityGroupId(stack, 'SecurityGroup', 'sg-0123456789'); + const vpc = new Vpc(stack, 'Vpc'); + + // WHEN + fn.addEventSource(new sources.SelfManagedKafkaEventSource( + { + bootstrapServers: bootstrapServers, + topic: kafkaTopic, + secret: secret, + startingPosition: lambda.StartingPosition.TRIM_HORIZON, + vpc: vpc, + vpcSubnets: { subnetType: SubnetType.PRIVATE }, + securityGroup: sg, + authenticationMethod: sources.AuthenticationMethod.SASL_SCRAM_256_AUTH, + })); + + expect(stack).to(haveResource('AWS::Lambda::EventSourceMapping', { + SourceAccessConfigurations: arrayWith( + { + Type: 'SASL_SCRAM_256_AUTH', + URI: { + Ref: 'SecretA720EF05', + }, + }, + ), + })); + + test.done(); + }, + }, + +} diff --git a/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts b/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts index d44ce1cbea1b4..239bf58671b7e 100644 --- a/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts +++ b/packages/@aws-cdk/aws-lambda/lib/event-source-mapping.ts @@ -4,12 +4,78 @@ import { IEventSourceDlq } from './dlq'; import { IFunction } from './function-base'; import { CfnEventSourceMapping } from './lambda.generated'; +/** + * The type of authentication protocol or the VPC components for your event source's SourceAccessConfiguration + * @see https://docs.aws.amazon.com/lambda/latest/dg/API_SourceAccessConfiguration.html#SSS-Type-SourceAccessConfiguration-Type + */ +export class SourceAccessConfigurationType { + + /** + * (MQ) The Secrets Manager secret that stores your broker credentials. + */ + public static readonly BASIC_AUTH = new SourceAccessConfigurationType('BASIC_AUTH'); + + /** + * The subnets associated with your VPC. Lambda connects to these subnets to fetch data from your Self-Managed Apache Kafka cluster. + */ + public static readonly VPC_SUBNET = new SourceAccessConfigurationType('VPC_SUBNET'); + + /** + * The VPC security group used to manage access to your Self-Managed Apache Kafka brokers. + */ + public static readonly VPC_SECURITY_GROUP = new SourceAccessConfigurationType('VPC_SECURITY_GROUP'); + + /** + * The Secrets Manager ARN of your secret key used for SASL SCRAM-256 authentication of your Self-Managed Apache Kafka brokers. + */ + public static readonly SASL_SCRAM_256_AUTH = new SourceAccessConfigurationType('SASL_SCRAM_256_AUTH'); + + /** + * The Secrets Manager ARN of your secret key used for SASL SCRAM-512 authentication of your Self-Managed Apache Kafka brokers. + */ + public static readonly SASL_SCRAM_512_AUTH = new SourceAccessConfigurationType('SASL_SCRAM_512_AUTH'); + + /** A custom source access configuration property */ + public static of(name: string): SourceAccessConfigurationType { + return new SourceAccessConfigurationType(name); + } + + /** + * The key to use in `SourceAccessConfigurationProperty.Type` property in CloudFormation + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-eventsourcemapping-sourceaccessconfiguration.html#cfn-lambda-eventsourcemapping-sourceaccessconfiguration-type + */ + public readonly type: string; + + private constructor(type: string) { + this.type = type; + } +} + +/** + * Specific settings like the authentication protocol or the VPC components to secure access to your event source. + */ +export interface SourceAccessConfiguration { + /** + * The type of authentication protocol or the VPC components for your event source. For example: "SASL_SCRAM_512_AUTH". + */ + readonly type: SourceAccessConfigurationType, + /** + * The value for your chosen configuration in type. + * For example: "URI": "arn:aws:secretsmanager:us-east-1:01234567890:secret:MyBrokerSecretName". + * The exact string depends on the type. + * @see SourceAccessConfigurationType + */ + readonly uri: string +} + export interface EventSourceMappingOptions { /** * The Amazon Resource Name (ARN) of the event source. Any record added to * this stream can invoke the Lambda function. + * + * @default - not set if using a self managed Kafka cluster, throws an error otherwise */ - readonly eventSourceArn: string; + readonly eventSourceArn?: string; /** * The largest number of records that AWS Lambda will retrieve from your event @@ -101,6 +167,23 @@ export interface EventSourceMappingOptions { * @default - no topic */ readonly kafkaTopic?: string; + + /** + * A list of host and port pairs that are the addresses of the Kafka brokers in a self managed "bootstrap" Kafka cluster + * that a Kafka client connects to initially to bootstrap itself. + * They are in the format `abc.example.com:9096`. + * + * @default - none + */ + readonly kafkaBootstrapServers?: string[] + + /** + * Specific settings like the authentication protocol or the VPC components to secure access to your event source. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-eventsourcemapping-sourceaccessconfiguration.html + * + * @default - none + */ + readonly sourceAccessConfigurations?: SourceAccessConfiguration[] } /** @@ -154,6 +237,18 @@ export class EventSourceMapping extends cdk.Resource implements IEventSourceMapp constructor(scope: Construct, id: string, props: EventSourceMappingProps) { super(scope, id); + if (props.eventSourceArn == undefined && props.kafkaBootstrapServers == undefined) { + throw new Error('Either eventSourceArn or kafkaBootstrapServers must be set'); + } + + if (props.eventSourceArn !== undefined && props.kafkaBootstrapServers !== undefined) { + throw new Error('eventSourceArn and kafkaBootstrapServers are mutually exclusive'); + } + + if (props.kafkaBootstrapServers && (props.kafkaBootstrapServers?.length < 1)) { + throw new Error('kafkaBootStrapServers must not be empty if set'); + } + if (props.maxBatchingWindow && props.maxBatchingWindow.toSeconds() > 300) { throw new Error(`maxBatchingWindow cannot be over 300 seconds, got ${props.maxBatchingWindow.toSeconds()}`); } @@ -183,6 +278,11 @@ export class EventSourceMapping extends cdk.Resource implements IEventSourceMapp }; } + let selfManagedEventSource; + if (props.kafkaBootstrapServers) { + selfManagedEventSource = { endpoints: { kafkaBootstrapServers: props.kafkaBootstrapServers } }; + } + const cfnEventSourceMapping = new CfnEventSourceMapping(this, 'Resource', { batchSize: props.batchSize, bisectBatchOnFunctionError: props.bisectBatchOnError, @@ -196,6 +296,8 @@ export class EventSourceMapping extends cdk.Resource implements IEventSourceMapp maximumRetryAttempts: props.retryAttempts, parallelizationFactor: props.parallelizationFactor, topics: props.kafkaTopic !== undefined ? [props.kafkaTopic] : undefined, + sourceAccessConfigurations: props.sourceAccessConfigurations?.map((o) => {return { type: o.type.type, uri: o.uri };}), + selfManagedEventSource, }); this.eventSourceMappingId = cfnEventSourceMapping.ref; } diff --git a/packages/@aws-cdk/aws-lambda/test/event-source-mapping.test.ts b/packages/@aws-cdk/aws-lambda/test/event-source-mapping.test.ts index be42067f263f1..5d833a2d865c6 100644 --- a/packages/@aws-cdk/aws-lambda/test/event-source-mapping.test.ts +++ b/packages/@aws-cdk/aws-lambda/test/event-source-mapping.test.ts @@ -170,4 +170,95 @@ describe('event source mapping', () => { }], }); }); + + test('throws if neither eventSourceArn nor kafkaBootstrapServers are set', () => { + const stack = new cdk.Stack(); + const fn = new Function(stack, 'fn', { + handler: 'index.handler', + code: Code.fromInline('exports.handler = ${handler.toString()}'), + runtime: Runtime.NODEJS_10_X, + }); + + expect(() => new EventSourceMapping(stack, 'test', { + target: fn, + })).toThrow(/Either eventSourceArn or kafkaBootstrapServers must be set/); + }); + + test('throws if both eventSourceArn and kafkaBootstrapServers are set', () => { + const stack = new cdk.Stack(); + const fn = new Function(stack, 'fn', { + handler: 'index.handler', + code: Code.fromInline('exports.handler = ${handler.toString()}'), + runtime: Runtime.NODEJS_10_X, + }); + + expect(() => new EventSourceMapping(stack, 'test', { + eventSourceArn: '', + kafkaBootstrapServers: [], + target: fn, + })).toThrow(/eventSourceArn and kafkaBootstrapServers are mutually exclusive/); + }); + + test('throws if both kafkaBootstrapServers is set but empty', () => { + const stack = new cdk.Stack(); + const fn = new Function(stack, 'fn', { + handler: 'index.handler', + code: Code.fromInline('exports.handler = ${handler.toString()}'), + runtime: Runtime.NODEJS_10_X, + }); + + expect(() => new EventSourceMapping(stack, 'test', { + kafkaBootstrapServers: [], + target: fn, + })).toThrow(/kafkaBootStrapServers must not be empty if set/); + }); + + test('eventSourceArn appears in stack', () => { + const stack = new cdk.Stack(); + const topicNameParam = new cdk.CfnParameter(stack, 'TopicNameParam', { + type: 'String', + }); + + const fn = new Function(stack, 'fn', { + handler: 'index.handler', + code: Code.fromInline('exports.handler = ${handler.toString()}'), + runtime: Runtime.NODEJS_10_X, + }); + + let eventSourceArn = 'some-arn'; + + new EventSourceMapping(stack, 'test', { + target: fn, + eventSourceArn: eventSourceArn, + kafkaTopic: topicNameParam.valueAsString, + }); + + expect(stack).toHaveResourceLike('AWS::Lambda::EventSourceMapping', { + EventSourceArn: eventSourceArn, + }); + }); + + test('kafkaBootstrapServers appears in stack', () => { + const stack = new cdk.Stack(); + const topicNameParam = new cdk.CfnParameter(stack, 'TopicNameParam', { + type: 'String', + }); + + const fn = new Function(stack, 'fn', { + handler: 'index.handler', + code: Code.fromInline('exports.handler = ${handler.toString()}'), + runtime: Runtime.NODEJS_10_X, + }); + + let kafkaBootstrapServers = ['kafka-broker.example.com:9092']; + new EventSourceMapping(stack, 'test', { + target: fn, + kafkaBootstrapServers: kafkaBootstrapServers, + kafkaTopic: topicNameParam.valueAsString, + }); + + expect(stack).toHaveResourceLike('AWS::Lambda::EventSourceMapping', { + SelfManagedEventSource: { Endpoints: { KafkaBootstrapServers: kafkaBootstrapServers } }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-msk/README.md b/packages/@aws-cdk/aws-msk/README.md index 1de05861fc74f..51d93453f1eef 100644 --- a/packages/@aws-cdk/aws-msk/README.md +++ b/packages/@aws-cdk/aws-msk/README.md @@ -9,6 +9,14 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- diff --git a/packages/@aws-cdk/aws-msk/lib/cluster.ts b/packages/@aws-cdk/aws-msk/lib/cluster.ts new file mode 100644 index 0000000000000..313bd3de5b107 --- /dev/null +++ b/packages/@aws-cdk/aws-msk/lib/cluster.ts @@ -0,0 +1,34 @@ +import { IResource, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +/** + * Represents an MSK cluster + */ +export interface ICluster extends IResource { + /** + * the ARN of the MSK cluster + */ + readonly clusterArn: string; +} + +/** + * An MSK cluster + */ +export class Cluster { + /** + * Creates a Cluster construct that represents an existing MSK cluster. + * @param scope + * @param id + * @param clusterArn + */ + public static fromClusterArn(scope: Construct, id: string, clusterArn: string): ICluster { + class Imported extends Resource implements ICluster { + public readonly clusterArn: string; + constructor() { + super(scope, id); + this.clusterArn = clusterArn; + } + } + return new Imported(); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-msk/lib/index.ts b/packages/@aws-cdk/aws-msk/lib/index.ts index 3cf150cbd7076..9cc4acdc7d61f 100644 --- a/packages/@aws-cdk/aws-msk/lib/index.ts +++ b/packages/@aws-cdk/aws-msk/lib/index.ts @@ -1,2 +1,3 @@ +export * from './cluster'; // AWS::MSK CloudFormation Resources: export * from './msk.generated'; diff --git a/packages/@aws-cdk/aws-msk/package.json b/packages/@aws-cdk/aws-msk/package.json index 939b66619cda7..efa91da07d39f 100644 --- a/packages/@aws-cdk/aws-msk/package.json +++ b/packages/@aws-cdk/aws-msk/package.json @@ -90,7 +90,7 @@ "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false }, From 2c9f09a88557289e3b8ee06602e6dc656e77e8cd Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 16 Mar 2021 17:13:55 +0100 Subject: [PATCH 24/32] fix(core): `toJsonString()` cannot handle list intrinsics (#13544) The previous attempt at a fix missed one important case: the types of the values involved in the `{ Fn::Join }` expression didn't actually match up. They all needed to be strings, but the previous implentation just dropped list-typed values in there. Unfortunately, there is no way to do it correctly with just string manipulation in CloudFormation (`{ Fn::Join }` etc), so we'll have to resort to using a Custom Resource if we encounter list values. Actually fixes #13465. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../core/lib/private/cfn-utils-provider.ts | 33 ++++++++++++++++ .../lib/private/cfn-utils-provider/consts.ts | 7 +++- .../lib/private/cfn-utils-provider/index.ts | 11 ++++++ .../core/lib/private/cloudformation-lang.ts | 39 ++++++++++++++++--- .../core/test/cloudformation-json.test.ts | 6 ++- 5 files changed, 88 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts b/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts index 8200165fbfe34..04a68394fe2f3 100644 --- a/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts +++ b/packages/@aws-cdk/core/lib/private/cfn-utils-provider.ts @@ -1,5 +1,7 @@ import { Construct } from '../construct-compat'; +import { CustomResource } from '../custom-resource'; import { CustomResourceProvider, CustomResourceProviderRuntime } from '../custom-resource-provider'; +import { CfnUtilsResourceType } from './cfn-utils-provider/consts'; /** * A custom resource provider for CFN utilities such as `CfnJson`. @@ -11,4 +13,35 @@ export class CfnUtilsProvider extends Construct { codeDirectory: `${__dirname}/cfn-utils-provider`, }); } +} + +/** + * Utility functions provided by the CfnUtilsProvider + */ +export abstract class CfnUtils { + /** + * Encode a structure to JSON at CloudFormation deployment time + * + * This would have been suitable for the JSON-encoding of abitrary structures, however: + * + * - It uses a custom resource to do the encoding, and we'd rather not use a custom + * resource if we can avoid it. + * - It cannot be used to encode objects where the keys of the objects can contain + * tokens--because those cannot be represented in the JSON encoding that CloudFormation + * templates use. + * + * This helper is used by `CloudFormationLang.toJSON()` if and only if it encounters + * objects that cannot be stringified any other way. + */ + public static stringify(scope: Construct, id: string, value: any): string { + const resource = new CustomResource(scope, id, { + serviceToken: CfnUtilsProvider.getOrCreate(scope), + resourceType: CfnUtilsResourceType.CFN_JSON_STRINGIFY, + properties: { + Value: value, + }, + }); + + return resource.getAttString('Value'); + } } \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/private/cfn-utils-provider/consts.ts b/packages/@aws-cdk/core/lib/private/cfn-utils-provider/consts.ts index b1571cabd5b42..9718dcef40645 100644 --- a/packages/@aws-cdk/core/lib/private/cfn-utils-provider/consts.ts +++ b/packages/@aws-cdk/core/lib/private/cfn-utils-provider/consts.ts @@ -5,5 +5,10 @@ export const enum CfnUtilsResourceType { /** * CfnJson */ - CFN_JSON = 'Custom::AWSCDKCfnJson' + CFN_JSON = 'Custom::AWSCDKCfnJson', + + /** + * CfnJsonStringify + */ + CFN_JSON_STRINGIFY = 'Custom::AWSCDKCfnJsonStringify', } diff --git a/packages/@aws-cdk/core/lib/private/cfn-utils-provider/index.ts b/packages/@aws-cdk/core/lib/private/cfn-utils-provider/index.ts index 87bd6bb070e16..f082001f80159 100644 --- a/packages/@aws-cdk/core/lib/private/cfn-utils-provider/index.ts +++ b/packages/@aws-cdk/core/lib/private/cfn-utils-provider/index.ts @@ -9,6 +9,9 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent if (event.ResourceType === CfnUtilsResourceType.CFN_JSON) { return cfnJsonHandler(event); } + if (event.ResourceType === CfnUtilsResourceType.CFN_JSON_STRINGIFY) { + return cfnJsonStringifyHandler(event); + } throw new Error(`unexpected resource type "${event.ResourceType}`); } @@ -20,3 +23,11 @@ function cfnJsonHandler(event: AWSLambda.CloudFormationCustomResourceEvent) { }, }; } + +function cfnJsonStringifyHandler(event: AWSLambda.CloudFormationCustomResourceEvent) { + return { + Data: { + Value: JSON.stringify(event.ResourceProperties.Value), + }, + }; +} diff --git a/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts b/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts index 310a4632f4e8f..c2be580474426 100644 --- a/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts +++ b/packages/@aws-cdk/core/lib/private/cloudformation-lang.ts @@ -1,6 +1,8 @@ import { Lazy } from '../lazy'; import { DefaultTokenResolver, IFragmentConcatenator, IResolveContext } from '../resolvable'; +import { Stack } from '../stack'; import { Token } from '../token'; +import { CfnUtils } from './cfn-utils-provider'; import { INTRINSIC_KEY_PREFIX, ResolutionTypeHint, resolvedTypeHint } from './resolve'; /** @@ -170,7 +172,8 @@ function tokenAwareStringify(root: any, space: number, ctx: IResolveContext) { // 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); + renderIntrinsic(obj); + return; } return renderCollection('{', '}', definedEntries(obj), ([key, value]) => { @@ -211,12 +214,34 @@ function tokenAwareStringify(root: any, space: number, ctx: IResolveContext) { pushLiteral('"'); pushIntrinsic(deepQuoteStringLiterals(intrinsic)); pushLiteral('"'); - break; - - default: + return; + + case ResolutionTypeHint.LIST: + // We need this to look like: + // + // '{"listValue":' ++ STRINGIFY(CFN_EVAL({ Ref: MyList })) ++ '}' + // + // However, STRINGIFY would need to execute at CloudFormation deployment time, and that doesn't exist. + // + // We could *ALMOST* use: + // + // '{"listValue":["' ++ JOIN('","', { Ref: MyList }) ++ '"]}' + // + // But that has the unfortunate side effect that if `CFN_EVAL({ Ref: MyList }) == []`, then it would + // evaluate to `[""]`, which is a different value. Since CloudFormation does not have arbitrary + // conditionals there's no way to deal with this case properly. + // + // Therefore, if we encounter lists we need to defer to a custom resource to handle + // them properly at deploy time. + pushIntrinsic(CfnUtils.stringify(Stack.of(ctx.scope), `CdkJsonStringify${stringifyCounter++}`, intrinsic)); + return; + + case ResolutionTypeHint.NUMBER: pushIntrinsic(intrinsic); - break; + return; } + + throw new Error(`Unexpected type hint: ${resolvedTypeHint(intrinsic)}`); } /** @@ -391,4 +416,6 @@ function deepQuoteStringLiterals(x: any): any { function quoteString(s: string) { s = JSON.stringify(s); return s.substring(1, s.length - 1); -} \ No newline at end of file +} + +let stringifyCounter = 1; \ No newline at end of file diff --git a/packages/@aws-cdk/core/test/cloudformation-json.test.ts b/packages/@aws-cdk/core/test/cloudformation-json.test.ts index cb96020e04904..e9a0957a86269 100644 --- a/packages/@aws-cdk/core/test/cloudformation-json.test.ts +++ b/packages/@aws-cdk/core/test/cloudformation-json.test.ts @@ -103,7 +103,11 @@ describe('tokens that return literals', () => { // WHEN expect(stack.resolve(stack.toJsonString({ someList }))).toEqual({ - 'Fn::Join': ['', ['{"someList":', { Ref: 'Thing' }, '}']], + 'Fn::Join': ['', [ + '{"someList":', + { 'Fn::GetAtt': [expect.stringContaining('CdkJsonStringify'), 'Value'] }, + '}', + ]], }); }); From c997c0fa165df9b80dcdef47a0ba4e3c9ff0cbb8 Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Tue, 16 Mar 2021 18:31:30 +0100 Subject: [PATCH 25/32] chore: replace angle braces with HTML-safe entities (#13619) This is an attempt to make the documentation safer to parse and render. ---- *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-efs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-efs/README.md b/packages/@aws-cdk/aws-efs/README.md index 002ba355e51eb..444a3a69d7480 100644 --- a/packages/@aws-cdk/aws-efs/README.md +++ b/packages/@aws-cdk/aws-efs/README.md @@ -90,9 +90,9 @@ efs.AccessPoint.fromAccessPointAttributes(this, 'ap', { ⚠️ Notice: When importing an Access Point using `fromAccessPointAttributes()`, you must make sure the mount targets are deployed and their lifecycle state is `available`. Otherwise, you may encounter the following error when deploying: -> EFS file system referenced by access point has +> EFS file system <ARN of efs> referenced by access point <ARN of access point of EFS> has > mount targets created in all availability zones the function will execute in, but not all ->are in the available life cycle state yet. Please wait for them to become available and +> are in the available life cycle state yet. Please wait for them to become available and > try the request again. ### Connecting From a7acaf97144a7ee8f4bde54d4ab8207fbbd4ff2d Mon Sep 17 00:00:00 2001 From: Hsing-Hui Hsu Date: Tue, 16 Mar 2021 17:10:46 -0700 Subject: [PATCH 26/32] docs(aws-ecs): fix typo in example for classic LB (#13627) Closes #13583 ---- *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-ecs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index cc4bd432e7dba..5ad2855c8f642 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -449,10 +449,10 @@ const service = new ecs.Ec2Service(this, 'Service', { /* ... */ }); const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); lb.addListener({ externalPort: 80 }); -lb.addTarget(service.loadBalancerTarget{ +lb.addTarget(service.loadBalancerTarget({ containerName: 'MyContainer', containerPort: 80 -}); +})); ``` There are two higher-level constructs available which include a load balancer for you that can be found in the aws-ecs-patterns module: From 811ab6cd6da66743a84e0e08a53075be082366e3 Mon Sep 17 00:00:00 2001 From: Apoorv Munshi Date: Tue, 16 Mar 2021 17:48:06 -0700 Subject: [PATCH 27/32] feat(sns): enable passing PolicyDocument to TopicPolicy (#10559) Adds optional `policyDocument` prop to `TopicPolicyProps` to allow passing existing policy documents. fixes #7934 ---- *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-sns/README.md | 40 ++++++++++ packages/@aws-cdk/aws-sns/lib/policy.ts | 8 ++ packages/@aws-cdk/aws-sns/test/test.sns.ts | 91 ++++++++++++++++++++++ 3 files changed, 139 insertions(+) diff --git a/packages/@aws-cdk/aws-sns/README.md b/packages/@aws-cdk/aws-sns/README.md index b5d9f52c3d9b9..ca80f72f9aad3 100644 --- a/packages/@aws-cdk/aws-sns/README.md +++ b/packages/@aws-cdk/aws-sns/README.md @@ -131,3 +131,43 @@ codeCommitRepository.onCommit(new targets.SnsTopic(myTopic)); This will result in adding a target to the event rule and will also modify the topic resource policy to allow CloudWatch events to publish to the topic. + +## Topic Policy + +A topic policy is automatically created when `addToResourcePolicy` is called, if +one doesn't already exist. Using `addToResourcePolicy` is the simplest way to +add policies, but a `TopicPolicy` can also be created manually. + +```ts +const topic = new sns.Topic(stack, 'Topic'); +const topicPolicy = new sns.TopicPolicy(stack, 'TopicPolicy', { + topics: [topic], +}); + +topicPolicy.document.addStatements(new iam.PolicyStatement({ + actions: ["sns:Subscribe"], + principals: [new iam.AnyPrincipal()], + resources: [topic.topicArn], +})); +``` + +A policy document can also be passed on `TopicPolicy` construction + +```ts +const topic = new sns.Topic(stack, 'Topic'); +const policyDocument = new iam.PolicyDocument({ + assignSids: true, + statements: [ + new iam.PolicyStatement({ + actions: ["sns:Subscribe"], + principals: [new iam.AnyPrincipal()], + resources: [topic.topicArn] + }), + ], +}); + +const topicPolicy = new sns.TopicPolicy(this, 'Policy', { + topics: [topic], + policyDocument, +}); +``` diff --git a/packages/@aws-cdk/aws-sns/lib/policy.ts b/packages/@aws-cdk/aws-sns/lib/policy.ts index 7d93d863a75f0..03a791bd57814 100644 --- a/packages/@aws-cdk/aws-sns/lib/policy.ts +++ b/packages/@aws-cdk/aws-sns/lib/policy.ts @@ -12,6 +12,12 @@ export interface TopicPolicyProps { * The set of topics this policy applies to. */ readonly topics: ITopic[]; + /** + * IAM policy document to apply to topic(s). + * @default empty policy document + */ + readonly policyDocument?: PolicyDocument; + } /** @@ -32,6 +38,8 @@ export class TopicPolicy extends Resource { constructor(scope: Construct, id: string, props: TopicPolicyProps) { super(scope, id); + this.document = props.policyDocument ?? this.document; + new CfnTopicPolicy(this, 'Resource', { policyDocument: this.document, topics: props.topics.map(t => t.topicArn), diff --git a/packages/@aws-cdk/aws-sns/test/test.sns.ts b/packages/@aws-cdk/aws-sns/test/test.sns.ts index cc4b50aed717c..5261900f9059b 100644 --- a/packages/@aws-cdk/aws-sns/test/test.sns.ts +++ b/packages/@aws-cdk/aws-sns/test/test.sns.ts @@ -276,6 +276,97 @@ export = { test.done(); }, + 'TopicPolicy passed document'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'MyTopic'); + const ps = new iam.PolicyStatement({ + actions: ['service:statement0'], + principals: [new iam.ArnPrincipal('arn')], + }); + + // WHEN + new sns.TopicPolicy(stack, 'topicpolicy', { topics: [topic], policyDocument: new iam.PolicyDocument({ assignSids: true, statements: [ps] }) }); + + // THEN + expect(stack).toMatch({ + 'Resources': { + 'MyTopic86869434': { + 'Type': 'AWS::SNS::Topic', + }, + 'topicpolicyF8CF12FD': { + 'Type': 'AWS::SNS::TopicPolicy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'service:statement0', + 'Effect': 'Allow', + 'Principal': { 'AWS': 'arn' }, + 'Sid': '0', + }, + ], + 'Version': '2012-10-17', + }, + 'Topics': [ + { + 'Ref': 'MyTopic86869434', + }, + ], + }, + }, + }, + }); + + test.done(); + }, + + 'Add statements to policy'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'MyTopic'); + + // WHEN + const topicPolicy = new sns.TopicPolicy(stack, 'TopicPolicy', { + topics: [topic], + }); + topicPolicy.document.addStatements(new iam.PolicyStatement({ + actions: ['service:statement0'], + principals: [new iam.ArnPrincipal('arn')], + })); + + // THEN + expect(stack).toMatch({ + 'Resources': { + 'MyTopic86869434': { + 'Type': 'AWS::SNS::Topic', + }, + 'TopicPolicyA24B096F': { + 'Type': 'AWS::SNS::TopicPolicy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': 'service:statement0', + 'Effect': 'Allow', + 'Principal': { 'AWS': 'arn' }, + 'Sid': '0', + }, + ], + 'Version': '2012-10-17', + }, + 'Topics': [ + { + 'Ref': 'MyTopic86869434', + }, + ], + }, + }, + }, + }); + test.done(); + }, + 'topic resource policy includes unique SIDs'(test: Test) { const stack = new cdk.Stack(); From 3ad7054b84470a7ab7db3b3914e1a6b357d46fc0 Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Wed, 17 Mar 2021 10:37:39 +0100 Subject: [PATCH 28/32] feat(cfnspec): cloudformation spec v31.0.0 (#13633) Co-authored-by: AWS CDK Team --- packages/@aws-cdk/cfnspec/CHANGELOG.md | 83 +++ packages/@aws-cdk/cfnspec/cfn.version | 2 +- ...0_CloudFormationResourceSpecification.json | 487 ++++++++++++++++-- 3 files changed, 539 insertions(+), 33 deletions(-) diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index 1affea5b1d444..861146ecf1337 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,86 @@ +# CloudFormation Resource Specification v31.0.0 + +## New Resource Types + +* AWS::CE::AnomalyMonitor +* AWS::CE::AnomalySubscription +* AWS::CertificateManager::Account +* AWS::ECS::ClusterCapacityProviderAssociations +* AWS::RDS::DBProxyEndpoint + +## Attribute Changes + +* AWS::SSM::ResourceDataSync SyncName (__added__) + +## Property Changes + +* AWS::AppSync::GraphQLApi LambdaAuthorizerConfig (__added__) +* AWS::Backup::BackupPlan BackupPlanTags.PrimitiveType (__deleted__) +* AWS::Backup::BackupPlan BackupPlanTags.PrimitiveItemType (__added__) +* AWS::Backup::BackupPlan BackupPlanTags.Type (__added__) +* AWS::DynamoDB::Table KinesisStreamSpecification (__deleted__) +* AWS::DynamoDB::Table ContributorInsightsSpecification (__added__) +* AWS::EC2::LaunchTemplate TagSpecifications (__added__) +* AWS::ECS::CapacityProvider AutoScalingGroupProvider.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ECS::Cluster Configuration (__added__) +* AWS::ECS::Service EnableExecuteCommand (__added__) +* AWS::EFS::FileSystem AvailabilityZoneName (__added__) + +## Property Type Changes + +* AWS::DynamoDB::Table.KinesisStreamSpecification (__removed__) +* AWS::AppSync::GraphQLApi.LambdaAuthorizerConfig (__added__) +* AWS::DynamoDB::Table.ContributorInsightsSpecification (__added__) +* AWS::EC2::LaunchTemplate.TagSpecifications (__added__) +* AWS::ECS::Cluster.ClusterConfiguration (__added__) +* AWS::ECS::Cluster.ExecuteCommandConfiguration (__added__) +* AWS::ECS::Cluster.ExecuteCommandLogConfiguration (__added__) +* AWS::AppSync::GraphQLApi.AdditionalAuthenticationProvider LambdaAuthorizerConfig (__added__) +* AWS::Backup::BackupPlan.BackupPlanResourceType AdvancedBackupSettings.DuplicatesAllowed (__added__) +* AWS::Backup::BackupPlan.BackupPlanResourceType BackupPlanRule.DuplicatesAllowed (__added__) +* AWS::Backup::BackupPlan.BackupRuleResourceType CopyActions.DuplicatesAllowed (__added__) +* AWS::Backup::BackupPlan.BackupRuleResourceType RecoveryPointTags.PrimitiveType (__deleted__) +* AWS::Backup::BackupPlan.BackupRuleResourceType RecoveryPointTags.PrimitiveItemType (__added__) +* AWS::Backup::BackupPlan.BackupRuleResourceType RecoveryPointTags.Type (__added__) +* AWS::DynamoDB::Table.GlobalSecondaryIndex ContributorInsightsSpecification (__added__) +* AWS::ECS::CapacityProvider.AutoScalingGroupProvider ManagedScaling.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ECS::CapacityProvider.AutoScalingGroupProvider ManagedTerminationProtection.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ECS::CapacityProvider.ManagedScaling MaximumScalingStepSize.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ECS::CapacityProvider.ManagedScaling MinimumScalingStepSize.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ECS::CapacityProvider.ManagedScaling Status.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::ECS::CapacityProvider.ManagedScaling TargetCapacity.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::GameLift::Alias.RoutingStrategy Type.Required (__changed__) + * Old: false + * New: true +* AWS::IoT::TopicRule.S3Action CannedAcl (__added__) +* AWS::IoT::TopicRule.TopicRulePayload Actions.DuplicatesAllowed (__deleted__) +* AWS::IoT::TopicRule.TopicRulePayload RuleDisabled.Required (__changed__) + * Old: true + * New: false +* AWS::SSM::ResourceDataSync.AwsOrganizationsSource OrganizationSourceType.UpdateType (__changed__) + * Old: Immutable + * New: Mutable +* AWS::SSM::ResourceDataSync.AwsOrganizationsSource OrganizationalUnits.DuplicatesAllowed (__added__) +* AWS::SSM::ResourceDataSync.SyncSource SourceRegions.DuplicatesAllowed (__added__) +* AWS::SSM::ResourceDataSync.SyncSource SourceType.UpdateType (__changed__) + * Old: Immutable + * New: Mutable + + # CloudFormation Resource Specification v30.1.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index a75ef34cbaa5c..221a8da0b5798 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -30.1.0 +31.0.0 diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index 05d46dca703b8..c3d61d168d11e 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -6388,6 +6388,12 @@ "Required": true, "UpdateType": "Mutable" }, + "LambdaAuthorizerConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-additionalauthenticationprovider.html#cfn-appsync-graphqlapi-additionalauthenticationprovider-lambdaauthorizerconfig", + "Required": false, + "Type": "LambdaAuthorizerConfig", + "UpdateType": "Mutable" + }, "OpenIDConnectConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-additionalauthenticationprovider.html#cfn-appsync-graphqlapi-additionalauthenticationprovider-openidconnectconfig", "Required": false, @@ -6432,6 +6438,29 @@ } } }, + "AWS::AppSync::GraphQLApi.LambdaAuthorizerConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-lambdaauthorizerconfig.html", + "Properties": { + "AuthorizerResultTtlInSeconds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-lambdaauthorizerconfig.html#cfn-appsync-graphqlapi-lambdaauthorizerconfig-authorizerresultttlinseconds", + "PrimitiveType": "Double", + "Required": false, + "UpdateType": "Mutable" + }, + "AuthorizerUri": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-lambdaauthorizerconfig.html#cfn-appsync-graphqlapi-lambdaauthorizerconfig-authorizeruri", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "IdentityValidationExpression": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-lambdaauthorizerconfig.html#cfn-appsync-graphqlapi-lambdaauthorizerconfig-identityvalidationexpression", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::AppSync::GraphQLApi.LogConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-appsync-graphqlapi-logconfig.html", "Properties": { @@ -8238,6 +8267,7 @@ "Properties": { "AdvancedBackupSettings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-backup-backupplan-backupplanresourcetype.html#cfn-backup-backupplan-backupplanresourcetype-advancedbackupsettings", + "DuplicatesAllowed": true, "ItemType": "AdvancedBackupSettingResourceType", "Required": false, "Type": "List", @@ -8251,6 +8281,7 @@ }, "BackupPlanRule": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-backup-backupplan-backupplanresourcetype.html#cfn-backup-backupplan-backupplanresourcetype-backupplanrule", + "DuplicatesAllowed": true, "ItemType": "BackupRuleResourceType", "Required": true, "Type": "List", @@ -8269,6 +8300,7 @@ }, "CopyActions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-backup-backupplan-backupruleresourcetype.html#cfn-backup-backupplan-backupruleresourcetype-copyactions", + "DuplicatesAllowed": true, "ItemType": "CopyActionResourceType", "Required": false, "Type": "List", @@ -8282,8 +8314,9 @@ }, "RecoveryPointTags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-backup-backupplan-backupruleresourcetype.html#cfn-backup-backupplan-backupruleresourcetype-recoverypointtags", - "PrimitiveType": "Json", + "PrimitiveItemType": "String", "Required": false, + "Type": "Map", "UpdateType": "Mutable" }, "RuleName": { @@ -9302,6 +9335,29 @@ } } }, + "AWS::CE::AnomalySubscription.Subscriber": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ce-anomalysubscription-subscriber.html", + "Properties": { + "Address": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ce-anomalysubscription-subscriber.html#cfn-ce-anomalysubscription-subscriber-address", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Status": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ce-anomalysubscription-subscriber.html#cfn-ce-anomalysubscription-subscriber-status", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ce-anomalysubscription-subscriber.html#cfn-ce-anomalysubscription-subscriber-type", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::Cassandra::Table.BillingMode": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cassandra-table-billingmode.html", "Properties": { @@ -9370,6 +9426,17 @@ } } }, + "AWS::CertificateManager::Account.ExpiryEventsConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-certificatemanager-account-expiryeventsconfiguration.html", + "Properties": { + "DaysBeforeExpiry": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-certificatemanager-account-expiryeventsconfiguration.html#cfn-certificatemanager-account-expiryeventsconfiguration-daysbeforeexpiry", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::CertificateManager::Certificate.DomainValidationOption": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-certificatemanager-certificate-domainvalidationoption.html", "Properties": { @@ -15690,9 +15757,26 @@ } } }, + "AWS::DynamoDB::Table.ContributorInsightsSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-contributorinsightsspecification.html", + "Properties": { + "Enabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-contributorinsightsspecification.html#cfn-dynamodb-contributorinsightsspecification-enabled", + "PrimitiveType": "Boolean", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::DynamoDB::Table.GlobalSecondaryIndex": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-gsi.html", "Properties": { + "ContributorInsightsSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-gsi.html#cfn-dynamodb-contributorinsightsspecification-enabled", + "Required": false, + "Type": "ContributorInsightsSpecification", + "UpdateType": "Mutable" + }, "IndexName": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-gsi.html#cfn-dynamodb-gsi-indexname", "PrimitiveType": "String", @@ -15738,17 +15822,6 @@ } } }, - "AWS::DynamoDB::Table.KinesisStreamSpecification": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-kinesisstreamspecification.html", - "Properties": { - "StreamArn": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-kinesisstreamspecification.html#cfn-dynamodb-kinesisstreamspecification-streamarn", - "PrimitiveType": "String", - "Required": true, - "UpdateType": "Mutable" - } - } - }, "AWS::DynamoDB::Table.LocalSecondaryIndex": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-lsi.html", "Properties": { @@ -17341,6 +17414,13 @@ } } }, + "AWS::EC2::LaunchTemplate.TagSpecifications": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-tagspecifications.html", + "ItemType": "TagSpecification", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, "AWS::EC2::NetworkAclEntry.Icmp": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-networkaclentry-icmp.html", "Properties": { @@ -18954,13 +19034,13 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-capacityprovider-autoscalinggroupprovider.html#cfn-ecs-capacityprovider-autoscalinggroupprovider-managedscaling", "Required": false, "Type": "ManagedScaling", - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "ManagedTerminationProtection": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-capacityprovider-autoscalinggroupprovider.html#cfn-ecs-capacityprovider-autoscalinggroupprovider-managedterminationprotection", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" } } }, @@ -18971,25 +19051,25 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-capacityprovider-managedscaling.html#cfn-ecs-capacityprovider-managedscaling-maximumscalingstepsize", "PrimitiveType": "Integer", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "MinimumScalingStepSize": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-capacityprovider-managedscaling.html#cfn-ecs-capacityprovider-managedscaling-minimumscalingstepsize", "PrimitiveType": "Integer", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Status": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-capacityprovider-managedscaling.html#cfn-ecs-capacityprovider-managedscaling-status", "PrimitiveType": "String", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "TargetCapacity": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-capacityprovider-managedscaling.html#cfn-ecs-capacityprovider-managedscaling-targetcapacity", "PrimitiveType": "Integer", "Required": false, - "UpdateType": "Immutable" + "UpdateType": "Mutable" } } }, @@ -19016,6 +19096,17 @@ } } }, + "AWS::ECS::Cluster.ClusterConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clusterconfiguration.html", + "Properties": { + "ExecuteCommandConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clusterconfiguration.html#cfn-ecs-cluster-clusterconfiguration-executecommandconfiguration", + "Required": false, + "Type": "ExecuteCommandConfiguration", + "UpdateType": "Mutable" + } + } + }, "AWS::ECS::Cluster.ClusterSettings": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-clustersettings.html", "Properties": { @@ -19033,6 +19124,87 @@ } } }, + "AWS::ECS::Cluster.ExecuteCommandConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandconfiguration.html", + "Properties": { + "KmsKeyId": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandconfiguration.html#cfn-ecs-cluster-executecommandconfiguration-kmskeyid", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "LogConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandconfiguration.html#cfn-ecs-cluster-executecommandconfiguration-logconfiguration", + "Required": false, + "Type": "ExecuteCommandLogConfiguration", + "UpdateType": "Mutable" + }, + "Logging": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandconfiguration.html#cfn-ecs-cluster-executecommandconfiguration-logging", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::ECS::Cluster.ExecuteCommandLogConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandlogconfiguration.html", + "Properties": { + "CloudWatchEncryptionEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandlogconfiguration.html#cfn-ecs-cluster-executecommandlogconfiguration-cloudwatchencryptionenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "CloudWatchLogGroupName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandlogconfiguration.html#cfn-ecs-cluster-executecommandlogconfiguration-cloudwatchloggroupname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "S3BucketName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandlogconfiguration.html#cfn-ecs-cluster-executecommandlogconfiguration-s3bucketname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "S3EncryptionEnabled": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandlogconfiguration.html#cfn-ecs-cluster-executecommandlogconfiguration-s3encryptionenabled", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "S3KeyPrefix": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-cluster-executecommandlogconfiguration.html#cfn-ecs-cluster-executecommandlogconfiguration-s3keyprefix", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, + "AWS::ECS::ClusterCapacityProviderAssociations.CapacityProviderStrategy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-clustercapacityproviderassociations-capacityproviderstrategy.html", + "Properties": { + "Base": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-clustercapacityproviderassociations-capacityproviderstrategy.html#cfn-ecs-clustercapacityproviderassociations-capacityproviderstrategy-base", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "CapacityProvider": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-clustercapacityproviderassociations-capacityproviderstrategy.html#cfn-ecs-clustercapacityproviderassociations-capacityproviderstrategy-capacityprovider", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Weight": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-clustercapacityproviderassociations-capacityproviderstrategy.html#cfn-ecs-clustercapacityproviderassociations-capacityproviderstrategy-weight", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::ECS::Service.AwsVpcConfiguration": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ecs-service-awsvpcconfiguration.html", "Properties": { @@ -24261,7 +24433,7 @@ "Type": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-gamelift-alias-routingstrategy.html#cfn-gamelift-alias-routingstrategy-type", "PrimitiveType": "String", - "Required": false, + "Required": true, "UpdateType": "Mutable" } } @@ -28958,6 +29130,12 @@ "Required": true, "UpdateType": "Mutable" }, + "CannedAcl": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-topicrule-s3action.html#cfn-iot-topicrule-s3action-cannedacl", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, "Key": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-topicrule-s3action.html#cfn-iot-topicrule-s3action-key", "PrimitiveType": "String", @@ -29069,7 +29247,6 @@ "Properties": { "Actions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-topicrule-topicrulepayload.html#cfn-iot-topicrule-topicrulepayload-actions", - "DuplicatesAllowed": false, "ItemType": "Action", "Required": true, "Type": "List", @@ -29096,7 +29273,7 @@ "RuleDisabled": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-topicrule-topicrulepayload.html#cfn-iot-topicrule-topicrulepayload-ruledisabled", "PrimitiveType": "Boolean", - "Required": true, + "Required": false, "UpdateType": "Mutable" }, "Sql": { @@ -45366,6 +45543,23 @@ } } }, + "AWS::RDS::DBProxyEndpoint.TagFormat": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbproxyendpoint-tagformat.html", + "Properties": { + "Key": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbproxyendpoint-tagformat.html#cfn-rds-dbproxyendpoint-tagformat-key", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbproxyendpoint-tagformat.html#cfn-rds-dbproxyendpoint-tagformat-value", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + } + } + }, "AWS::RDS::DBProxyTargetGroup.ConnectionPoolConfigurationInfoFormat": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-dbproxytargetgroup-connectionpoolconfigurationinfoformat.html", "Properties": { @@ -48346,10 +48540,11 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-resourcedatasync-awsorganizationssource.html#cfn-ssm-resourcedatasync-awsorganizationssource-organizationsourcetype", "PrimitiveType": "String", "Required": true, - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "OrganizationalUnits": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-resourcedatasync-awsorganizationssource.html#cfn-ssm-resourcedatasync-awsorganizationssource-organizationalunits", + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": false, "Type": "List", @@ -48409,6 +48604,7 @@ }, "SourceRegions": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-resourcedatasync-syncsource.html#cfn-ssm-resourcedatasync-syncsource-sourceregions", + "DuplicatesAllowed": true, "PrimitiveItemType": "String", "Required": true, "Type": "List", @@ -48418,7 +48614,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ssm-resourcedatasync-syncsource.html#cfn-ssm-resourcedatasync-syncsource-sourcetype", "PrimitiveType": "String", "Required": true, - "UpdateType": "Immutable" + "UpdateType": "Mutable" } } }, @@ -53649,7 +53845,7 @@ } } }, - "ResourceSpecificationVersion": "30.1.0", + "ResourceSpecificationVersion": "31.0.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -57283,6 +57479,12 @@ "Required": true, "UpdateType": "Mutable" }, + "LambdaAuthorizerConfig": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-graphqlapi.html#cfn-appsync-graphqlapi-lambdaauthorizerconfig", + "Required": false, + "Type": "LambdaAuthorizerConfig", + "UpdateType": "Mutable" + }, "LogConfig": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-appsync-graphqlapi.html#cfn-appsync-graphqlapi-logconfig", "Required": false, @@ -58350,8 +58552,9 @@ }, "BackupPlanTags": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-backup-backupplan.html#cfn-backup-backupplan-backupplantags", - "PrimitiveType": "Json", + "PrimitiveItemType": "String", "Required": false, + "Type": "Map", "UpdateType": "Mutable" } } @@ -58586,6 +58789,97 @@ } } }, + "AWS::CE::AnomalyMonitor": { + "Attributes": { + "CreationDate": { + "PrimitiveType": "String" + }, + "DimensionalValueCount": { + "PrimitiveType": "Integer" + }, + "LastEvaluatedDate": { + "PrimitiveType": "String" + }, + "LastUpdatedDate": { + "PrimitiveType": "String" + }, + "MonitorArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ce-anomalymonitor.html", + "Properties": { + "MonitorDimension": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ce-anomalymonitor.html#cfn-ce-anomalymonitor-monitordimension", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "MonitorName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ce-anomalymonitor.html#cfn-ce-anomalymonitor-monitorname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "MonitorSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ce-anomalymonitor.html#cfn-ce-anomalymonitor-monitorspecification", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "MonitorType": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ce-anomalymonitor.html#cfn-ce-anomalymonitor-monitortype", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + }, + "AWS::CE::AnomalySubscription": { + "Attributes": { + "AccountId": { + "PrimitiveType": "String" + }, + "SubscriptionArn": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ce-anomalysubscription.html", + "Properties": { + "Frequency": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ce-anomalysubscription.html#cfn-ce-anomalysubscription-frequency", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "MonitorArnList": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ce-anomalysubscription.html#cfn-ce-anomalysubscription-monitorarnlist", + "PrimitiveItemType": "String", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "Subscribers": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ce-anomalysubscription.html#cfn-ce-anomalysubscription-subscribers", + "ItemType": "Subscriber", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "SubscriptionName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ce-anomalysubscription.html#cfn-ce-anomalysubscription-subscriptionname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Threshold": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ce-anomalysubscription.html#cfn-ce-anomalysubscription-threshold", + "PrimitiveType": "Double", + "Required": true, + "UpdateType": "Mutable" + } + } + }, "AWS::CE::CostCategory": { "Attributes": { "Arn": { @@ -58697,6 +58991,22 @@ } } }, + "AWS::CertificateManager::Account": { + "Attributes": { + "AccountId": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-certificatemanager-account.html", + "Properties": { + "ExpiryEventsConfiguration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-certificatemanager-account.html#cfn-certificatemanager-account-expiryeventsconfiguration", + "Required": true, + "Type": "ExpiryEventsConfiguration", + "UpdateType": "Mutable" + } + } + }, "AWS::CertificateManager::Certificate": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-certificatemanager-certificate.html", "Properties": { @@ -63708,6 +64018,12 @@ "Required": false, "UpdateType": "Mutable" }, + "ContributorInsightsSpecification": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html#cfn-dynamodb-contributorinsightsspecification-enabled", + "Required": false, + "Type": "ContributorInsightsSpecification", + "UpdateType": "Mutable" + }, "GlobalSecondaryIndexes": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html#cfn-dynamodb-table-gsi", "DuplicatesAllowed": true, @@ -63724,12 +64040,6 @@ "Type": "List", "UpdateType": "Immutable" }, - "KinesisStreamSpecification": { - "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html#cfn-dynamodb-table-kinesisstreamspecification", - "Required": false, - "Type": "KinesisStreamSpecification", - "UpdateType": "Mutable" - }, "LocalSecondaryIndexes": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html#cfn-dynamodb-table-lsi", "DuplicatesAllowed": true, @@ -64742,6 +65052,12 @@ "PrimitiveType": "String", "Required": false, "UpdateType": "Immutable" + }, + "TagSpecifications": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-launchtemplate.html#cfn-ec2-launchtemplate-tagspecifications", + "Required": false, + "Type": "TagSpecifications", + "UpdateType": "Mutable" } } }, @@ -66868,7 +67184,7 @@ "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-capacityprovider.html#cfn-ecs-capacityprovider-autoscalinggroupprovider", "Required": true, "Type": "AutoScalingGroupProvider", - "UpdateType": "Immutable" + "UpdateType": "Mutable" }, "Name": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-capacityprovider.html#cfn-ecs-capacityprovider-name", @@ -66913,6 +67229,12 @@ "Type": "List", "UpdateType": "Mutable" }, + "Configuration": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-cluster.html#cfn-ecs-cluster-configuration", + "Required": false, + "Type": "ClusterConfiguration", + "UpdateType": "Mutable" + }, "DefaultCapacityProviderStrategy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-cluster.html#cfn-ecs-cluster-defaultcapacityproviderstrategy", "ItemType": "CapacityProviderStrategyItem", @@ -66929,6 +67251,32 @@ } } }, + "AWS::ECS::ClusterCapacityProviderAssociations": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-clustercapacityproviderassociations.html", + "Properties": { + "CapacityProviders": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-clustercapacityproviderassociations.html#cfn-ecs-clustercapacityproviderassociations-capacityproviders", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + }, + "Cluster": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-clustercapacityproviderassociations.html#cfn-ecs-clustercapacityproviderassociations-cluster", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "DefaultCapacityProviderStrategy": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-clustercapacityproviderassociations.html#cfn-ecs-clustercapacityproviderassociations-defaultcapacityproviderstrategy", + "ItemType": "CapacityProviderStrategy", + "Required": true, + "Type": "List", + "UpdateType": "Mutable" + } + } + }, "AWS::ECS::PrimaryTaskSet": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-primarytaskset.html", "Properties": { @@ -66997,6 +67345,12 @@ "Required": false, "UpdateType": "Immutable" }, + "EnableExecuteCommand": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-service.html#cfn-ecs-service-enableexecutecommand", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, "HealthCheckGracePeriodSeconds": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-service.html#cfn-ecs-service-healthcheckgraceperiodseconds", "PrimitiveType": "Integer", @@ -67333,6 +67687,12 @@ }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-efs-filesystem.html", "Properties": { + "AvailabilityZoneName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-efs-filesystem.html#cfn-efs-filesystem-availabilityzonename", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, "BackupPolicy": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-efs-filesystem.html#cfn-efs-filesystem-backuppolicy", "Required": false, @@ -81406,6 +81766,64 @@ } } }, + "AWS::RDS::DBProxyEndpoint": { + "Attributes": { + "DBProxyEndpointArn": { + "PrimitiveType": "String" + }, + "Endpoint": { + "PrimitiveType": "String" + }, + "IsDefault": { + "PrimitiveType": "Boolean" + }, + "VpcId": { + "PrimitiveType": "String" + } + }, + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxyendpoint.html", + "Properties": { + "DBProxyEndpointName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxyendpoint.html#cfn-rds-dbproxyendpoint-dbproxyendpointname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "DBProxyName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxyendpoint.html#cfn-rds-dbproxyendpoint-dbproxyname", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + }, + "Tags": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxyendpoint.html#cfn-rds-dbproxyendpoint-tags", + "ItemType": "TagFormat", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "TargetRole": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxyendpoint.html#cfn-rds-dbproxyendpoint-targetrole", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Immutable" + }, + "VpcSecurityGroupIds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxyendpoint.html#cfn-rds-dbproxyendpoint-vpcsecuritygroupids", + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "VpcSubnetIds": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbproxyendpoint.html#cfn-rds-dbproxyendpoint-vpcsubnetids", + "PrimitiveItemType": "String", + "Required": true, + "Type": "List", + "UpdateType": "Immutable" + } + } + }, "AWS::RDS::DBProxyTargetGroup": { "Attributes": { "TargetGroupArn": { @@ -83882,6 +84300,11 @@ } }, "AWS::SSM::ResourceDataSync": { + "Attributes": { + "SyncName": { + "PrimitiveType": "String" + } + }, "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ssm-resourcedatasync.html", "Properties": { "BucketName": { From 580fb141c1fe07dcaa24d5b0727be20cd77bdb44 Mon Sep 17 00:00:00 2001 From: Kyle Roach Date: Wed, 17 Mar 2021 07:45:38 -0300 Subject: [PATCH 29/32] feat(apigatewayv2): http api - default authorizer options (#13172) Allows setting an authorizer and authorization scopes that will be applied to all routes of the api. @nija-at this is the bit we forgot In the first PR. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-apigatewayv2-authorizers/README.md | 90 +++++++++++++- .../test/http/integ.user-pool.expected.json | 33 +++-- .../test/http/integ.alb.expected.json | 37 +++--- .../test/http/integ.http-proxy.expected.json | 74 ++++++----- .../http/integ.lambda-proxy.expected.json | 33 +++-- .../test/http/integ.nlb.expected.json | 37 +++--- .../integ.service-discovery.expected.json | 41 +++--- .../@aws-cdk/aws-apigatewayv2/lib/http/api.ts | 48 +++++-- .../aws-apigatewayv2/lib/http/authorizer.ts | 18 ++- .../aws-apigatewayv2/lib/http/route.ts | 14 ++- .../aws-apigatewayv2/test/http/api.test.ts | 117 +++++++++++++++++- .../integ.call-http-api.expected.json | 1 - 12 files changed, 391 insertions(+), 152 deletions(-) diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/README.md b/packages/@aws-cdk/aws-apigatewayv2-authorizers/README.md index 9591ecfe4ffde..ae44e5e5d97fc 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-authorizers/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/README.md @@ -1,4 +1,5 @@ # AWS APIGatewayv2 Authorizers + --- @@ -17,24 +18,105 @@ ## Table of Contents +- [Introduction](#introduction) - [HTTP APIs](#http-apis) + - [Default Authorization](#default-authorization) + - [Route Authorization](#route-authorization) - [JWT Authorizers](#jwt-authorizers) - [User Pool Authorizer](#user-pool-authorizer) -## HTTP APIs +## Introduction API Gateway supports multiple mechanisms for controlling and managing access to your HTTP API. They are mainly classified into Lambda Authorizers, JWT authorizers and standard AWS IAM roles and policies. More information is available at [Controlling and managing access to an HTTP API](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-access-control.html). +## HTTP APIs + +Access control for Http Apis is managed by restricting which routes can be invoked via. + +Authorizers, and scopes can either be applied to the api, or specifically for each route. + +### Default Authorization + +When using default authorization, all routes of the api will inherit the configuration. + +In the example below, all routes will require the `manage:books` scope present in order to invoke the integration. + +```ts +const authorizer = new HttpJwtAuthorizer({ + ... +}); + +const api = new HttpApi(stack, 'HttpApi', { + defaultAuthorizer: authorizer, + defaultAuthorizationScopes: ['manage:books'], +}); +``` + +### Route Authorization + +Authorization can also configured for each Route. When a route authorization is configured, it takes precedence over default authorization. + +The example below showcases default authorization, along with route authorization. It also shows how to remove authorization entirely for a route. + +- `GET /books` and `GET /books/{id}` use the default authorizer settings on the api +- `POST /books` will require the [write:books] scope +- `POST /login` removes the default authorizer (unauthenticated route) + +```ts +const authorizer = new HttpJwtAuthorizer({ + ... +}); + +const api = new HttpApi(stack, 'HttpApi', { + defaultAuthorizer: authorizer, + defaultAuthorizationScopes: ['read:books'], +}); + +api.addRoutes({ + ... + path: '/books', + method: 'get', +}); + +api.addRoutes({ + ... + path: '/books/{id}', + method: 'get', +}); + +api.addRoutes({ + ... + path: '/books', + method: 'post', + authorizationScopes: ['write:books'] +}); + +api.addRoutes({ + ... + path: '/login', + method: 'post', + authorizer: new NoneAuthorizer(), +}); +``` + ## JWT Authorizers JWT authorizers allow the use of JSON Web Tokens (JWTs) as part of [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) and [OAuth 2.0](https://oauth.net/2/) frameworks to allow and restrict clients from accessing HTTP APIs. -When configured on a route, the API Gateway service validates the JWTs submitted by the client, and allows or denies access based on its content. +When configured, API Gateway validates the JWT submitted by the client, and allows or denies access based on its content. + +The location of the token is defined by the `identitySource` which defaults to the http `Authorization` header. However it also +[supports a number of other options](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html#http-api-lambda-authorizer.identity-sources). +It then decodes the JWT and validates the signature and claims, against the options defined in the authorizer and route (scopes). +For more information check the [JWT Authorizer documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html). + +Clients that fail authorization are presented with either 2 responses: -API gateway uses the `identitySource` to determine where to look for the token. By default it checks the http `Authorization` header. However it also [supports a number of other options](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html#http-api-lambda-authorizer.identity-sources). It then decodes the JWT and validates the signature and claims, against the options defined in the authorizer and route (scopes). For more information check the [JWT Authorizer documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html). +- `401 - Unauthorized` - When the JWT validation fails +- `403 - Forbidden` - When the JWT validation is successful but the required scopes are not met ```ts const authorizer = new HttpJwtAuthorizer({ @@ -58,7 +140,7 @@ api.addRoutes({ User Pool Authorizer is a type of JWT Authorizer that uses a Cognito user pool and app client to control who can access your Api. After a successful authorization from the app client, the generated access token will be used as the JWT. Clients accessing an API that uses a user pool authorizer must first sign in to a user pool and obtain an identity or access token. -They must then use this token in the `Authorization` header of the API call. More information is available at [using Amazon Cognito user +They must then use this token in the specified `identitySource` for the API call. More information is available at [using Amazon Cognito user pools as authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html). ```ts diff --git a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.expected.json b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.expected.json index 17e2ae9976378..c3ddc2fbc6e94 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-authorizers/test/http/integ.user-pool.expected.json @@ -54,6 +54,22 @@ } } }, + "MyHttpApiGETHttpIntegration6f095b8469365f72e33fa33d9711b140516EBE31": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "MyHttpApi8AEAAC21" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "lambda8B5974B5", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + } + }, "MyHttpApiGETE0EFC6F8": { "Type": "AWS::ApiGatewayV2::Route", "Properties": { @@ -61,7 +77,6 @@ "Ref": "MyHttpApi8AEAAC21" }, "RouteKey": "GET /", - "AuthorizationScopes": [], "AuthorizationType": "JWT", "AuthorizerId": { "Ref": "MyHttpApiUserPoolAuthorizer8754262B" @@ -79,22 +94,6 @@ } } }, - "MyHttpApiGETHttpIntegration6f095b8469365f72e33fa33d9711b140516EBE31": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "MyHttpApi8AEAAC21" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "lambda8B5974B5", - "Arn" - ] - }, - "PayloadFormatVersion": "2.0" - } - }, "MyHttpApiUserPoolAuthorizer8754262B": { "Type": "AWS::ApiGatewayV2::Authorizer", "Properties": { diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json index e0ffbc32a3349..ae65d49847d54 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json @@ -608,6 +608,24 @@ "ProtocolType": "HTTP" } }, + "HttpProxyPrivateApiDefaultRouteHttpIntegration1a580b19954e4317026ffbce1f7d5ade7A32685B": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + "IntegrationType": "HTTP_PROXY", + "ConnectionId": { + "Ref": "HttpProxyPrivateApiVpcLink190366CAE" + }, + "ConnectionType": "VPC_LINK", + "IntegrationMethod": "ANY", + "IntegrationUri": { + "Ref": "lblistener657ADDEC" + }, + "PayloadFormatVersion": "1.0" + } + }, "HttpProxyPrivateApiDefaultRoute1BDCA252": { "Type": "AWS::ApiGatewayV2::Route", "Properties": { @@ -615,7 +633,6 @@ "Ref": "HttpProxyPrivateApiA55E154D" }, "RouteKey": "$default", - "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", @@ -647,24 +664,6 @@ "SecurityGroupIds": [] } }, - "HttpProxyPrivateApiDefaultRouteHttpIntegration1a580b19954e4317026ffbce1f7d5ade7A32685B": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "HttpProxyPrivateApiA55E154D" - }, - "IntegrationType": "HTTP_PROXY", - "ConnectionId": { - "Ref": "HttpProxyPrivateApiVpcLink190366CAE" - }, - "ConnectionType": "VPC_LINK", - "IntegrationMethod": "ANY", - "IntegrationUri": { - "Ref": "lblistener657ADDEC" - }, - "PayloadFormatVersion": "1.0" - } - }, "HttpProxyPrivateApiDefaultStage18B3706E": { "Type": "AWS::ApiGatewayV2::Stage", "Properties": { diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.http-proxy.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.http-proxy.expected.json index e5220fe2959b7..378e7b2395f03 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.http-proxy.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.http-proxy.expected.json @@ -94,6 +94,22 @@ } } }, + "LambdaProxyApiDefaultRouteHttpIntegration70df0ec52c3e3b6bbc96e64ce3a05f24EE575CBA": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "LambdaProxyApi67594471" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "AlwaysSuccess099EAB05", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + } + }, "LambdaProxyApiDefaultRoute1EB30A46": { "Type": "AWS::ApiGatewayV2::Route", "Properties": { @@ -101,7 +117,6 @@ "Ref": "LambdaProxyApi67594471" }, "RouteKey": "$default", - "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", @@ -115,22 +130,6 @@ } } }, - "LambdaProxyApiDefaultRouteHttpIntegration70df0ec52c3e3b6bbc96e64ce3a05f24EE575CBA": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "LambdaProxyApi67594471" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "AlwaysSuccess099EAB05", - "Arn" - ] - }, - "PayloadFormatVersion": "2.0" - } - }, "LambdaProxyApiDefaultStage07C38681": { "Type": "AWS::ApiGatewayV2::Stage", "Properties": { @@ -148,27 +147,6 @@ "ProtocolType": "HTTP" } }, - "HttpProxyApiDefaultRoute8AF66B5C": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "HttpProxyApiD0217C67" - }, - "RouteKey": "$default", - "AuthorizationScopes": [], - "Target": { - "Fn::Join": [ - "", - [ - "integrations/", - { - "Ref": "HttpProxyApiDefaultRouteHttpIntegration8eeecf9ecdb91f31bebf6bd54fb711a41921AB82" - } - ] - ] - } - } - }, "HttpProxyApiDefaultRouteHttpIntegration8eeecf9ecdb91f31bebf6bd54fb711a41921AB82": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { @@ -200,6 +178,26 @@ "PayloadFormatVersion": "1.0" } }, + "HttpProxyApiDefaultRoute8AF66B5C": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpProxyApiD0217C67" + }, + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpProxyApiDefaultRouteHttpIntegration8eeecf9ecdb91f31bebf6bd54fb711a41921AB82" + } + ] + ] + } + } + }, "HttpProxyApiDefaultStageA88F9DE3": { "Type": "AWS::ApiGatewayV2::Stage", "Properties": { diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.lambda-proxy.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.lambda-proxy.expected.json index f5bc444d929c3..58e37b0f64e0a 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.lambda-proxy.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.lambda-proxy.expected.json @@ -94,6 +94,22 @@ } } }, + "LambdaProxyApiDefaultRouteHttpIntegration70df0ec52c3e3b6bbc96e64ce3a05f24EE575CBA": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "LambdaProxyApi67594471" + }, + "IntegrationType": "AWS_PROXY", + "IntegrationUri": { + "Fn::GetAtt": [ + "AlwaysSuccess099EAB05", + "Arn" + ] + }, + "PayloadFormatVersion": "2.0" + } + }, "LambdaProxyApiDefaultRoute1EB30A46": { "Type": "AWS::ApiGatewayV2::Route", "Properties": { @@ -101,7 +117,6 @@ "Ref": "LambdaProxyApi67594471" }, "RouteKey": "$default", - "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", @@ -115,22 +130,6 @@ } } }, - "LambdaProxyApiDefaultRouteHttpIntegration70df0ec52c3e3b6bbc96e64ce3a05f24EE575CBA": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "LambdaProxyApi67594471" - }, - "IntegrationType": "AWS_PROXY", - "IntegrationUri": { - "Fn::GetAtt": [ - "AlwaysSuccess099EAB05", - "Arn" - ] - }, - "PayloadFormatVersion": "2.0" - } - }, "LambdaProxyApiDefaultStage07C38681": { "Type": "AWS::ApiGatewayV2::Stage", "Properties": { diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json index 0446031c61a19..aed54a5a8395c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json @@ -573,6 +573,24 @@ "ProtocolType": "HTTP" } }, + "HttpProxyPrivateApiDefaultRouteHttpIntegration1a580b19954e4317026ffbce1f7d5ade7A32685B": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + "IntegrationType": "HTTP_PROXY", + "ConnectionId": { + "Ref": "HttpProxyPrivateApiVpcLink190366CAE" + }, + "ConnectionType": "VPC_LINK", + "IntegrationMethod": "ANY", + "IntegrationUri": { + "Ref": "lblistener657ADDEC" + }, + "PayloadFormatVersion": "1.0" + } + }, "HttpProxyPrivateApiDefaultRoute1BDCA252": { "Type": "AWS::ApiGatewayV2::Route", "Properties": { @@ -580,7 +598,6 @@ "Ref": "HttpProxyPrivateApiA55E154D" }, "RouteKey": "$default", - "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", @@ -612,24 +629,6 @@ "SecurityGroupIds": [] } }, - "HttpProxyPrivateApiDefaultRouteHttpIntegration1a580b19954e4317026ffbce1f7d5ade7A32685B": { - "Type": "AWS::ApiGatewayV2::Integration", - "Properties": { - "ApiId": { - "Ref": "HttpProxyPrivateApiA55E154D" - }, - "IntegrationType": "HTTP_PROXY", - "ConnectionId": { - "Ref": "HttpProxyPrivateApiVpcLink190366CAE" - }, - "ConnectionType": "VPC_LINK", - "IntegrationMethod": "ANY", - "IntegrationUri": { - "Ref": "lblistener657ADDEC" - }, - "PayloadFormatVersion": "1.0" - } - }, "HttpProxyPrivateApiDefaultStage18B3706E": { "Type": "AWS::ApiGatewayV2::Stage", "Properties": { diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json index 28b636ef54808..1aaf644336b8c 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json @@ -574,27 +574,6 @@ "ProtocolType": "HTTP" } }, - "HttpProxyPrivateApiDefaultRoute1BDCA252": { - "Type": "AWS::ApiGatewayV2::Route", - "Properties": { - "ApiId": { - "Ref": "HttpProxyPrivateApiA55E154D" - }, - "RouteKey": "$default", - "AuthorizationScopes": [], - "Target": { - "Fn::Join": [ - "", - [ - "integrations/", - { - "Ref": "HttpProxyPrivateApiDefaultRouteHttpIntegrationa5ec5390ca688d567e9449daf58afc6f6DEAA8A8" - } - ] - ] - } - } - }, "HttpProxyPrivateApiDefaultRouteHttpIntegrationa5ec5390ca688d567e9449daf58afc6f6DEAA8A8": { "Type": "AWS::ApiGatewayV2::Integration", "Properties": { @@ -616,6 +595,26 @@ "PayloadFormatVersion": "1.0" } }, + "HttpProxyPrivateApiDefaultRoute1BDCA252": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpProxyPrivateApiDefaultRouteHttpIntegrationa5ec5390ca688d567e9449daf58afc6f6DEAA8A8" + } + ] + ] + } + } + }, "HttpProxyPrivateApiDefaultStage18B3706E": { "Type": "AWS::ApiGatewayV2::Stage", "Properties": { diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index b74c00e5824fb..24d250dfd6eda 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -83,6 +83,20 @@ export interface HttpApiProps { * @default false execute-api endpoint enabled. */ readonly disableExecuteApiEndpoint?: boolean; + + /** + * Default Authorizer to applied to all routes in the gateway + * + * @default - No authorizer + */ + readonly defaultAuthorizer?: IHttpRouteAuthorizer; + + /** + * Default OIDC scopes attached to all routes in the gateway, unless explicitly configured on the route. + * + * @default - no default authorization scopes + */ + readonly defaultAuthorizationScopes?: string[]; } /** @@ -143,15 +157,20 @@ export interface AddRoutesOptions extends BatchHttpRouteOptions { /** * Authorizer to be associated to these routes. - * @default - No authorizer + * + * Use NoneAuthorizer to remove the default authorizer for the api + * + * @default - uses the default authorizer if one is specified on the HttpApi */ readonly authorizer?: IHttpRouteAuthorizer; /** * The list of OIDC scopes to include in the authorization. * - * These scopes will be merged with the scopes from the attached authorizer - * @default - no additional authorization scopes + * These scopes will override the default authorization scopes on the gateway. + * Set to [] to remove default scopes + * + * @default - uses defaultAuthorizationScopes if configured on the API, otherwise none. */ readonly authorizationScopes?: string[]; } @@ -258,6 +277,9 @@ export class HttpApi extends HttpApiBase { private readonly _apiEndpoint: string; + private readonly defaultAuthorizer?: IHttpRouteAuthorizer; + private readonly defaultAuthorizationScopes?: string[]; + constructor(scope: Construct, id: string, props?: HttpApiProps) { super(scope, id); @@ -300,6 +322,8 @@ export class HttpApi extends HttpApiBase { this.apiId = resource.ref; this.httpApiId = resource.ref; this._apiEndpoint = resource.attrApiEndpoint; + this.defaultAuthorizer = props?.defaultAuthorizer; + this.defaultAuthorizationScopes = props?.defaultAuthorizationScopes; if (props?.defaultIntegration) { new HttpRoute(this, 'DefaultRoute', { @@ -363,12 +387,16 @@ export class HttpApi extends HttpApiBase { */ public addRoutes(options: AddRoutesOptions): HttpRoute[] { const methods = options.methods ?? [HttpMethod.ANY]; - return methods.map((method) => new HttpRoute(this, `${method}${options.path}`, { - httpApi: this, - routeKey: HttpRouteKey.with(options.path, method), - integration: options.integration, - authorizer: options.authorizer, - authorizationScopes: options.authorizationScopes, - })); + return methods.map((method) => { + const authorizationScopes = options.authorizationScopes ?? this.defaultAuthorizationScopes; + + return new HttpRoute(this, `${method}${options.path}`, { + httpApi: this, + routeKey: HttpRouteKey.with(options.path, method), + integration: options.integration, + authorizer: options.authorizer ?? this.defaultAuthorizer, + authorizationScopes, + }); + }); } } diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts index f63c2e3a96583..297abf12e78e2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/authorizer.ts @@ -15,6 +15,9 @@ export enum HttpAuthorizerType { /** Lambda Authorizer */ LAMBDA = 'REQUEST', + + /** No authorizer */ + NONE = 'NONE' } /** @@ -145,8 +148,10 @@ export interface HttpRouteAuthorizerBindOptions { export interface HttpRouteAuthorizerConfig { /** * The authorizer id + * + * @default - No authorizer id (useful for AWS_IAM route authorizer) */ - readonly authorizerId: string; + readonly authorizerId?: string; /** * The type of authorization */ @@ -172,3 +177,14 @@ function undefinedIfNoKeys(obj: A): A | undefined { const allUndefined = Object.values(obj).every(val => val === undefined); return allUndefined ? undefined : obj; } + +/** + * Explicitly configure no authorizers on specific HTTP API routes. + */ +export class HttpNoneAuthorizer implements IHttpRouteAuthorizer { + public bind(_: HttpRouteAuthorizerBindOptions): HttpRouteAuthorizerConfig { + return { + authorizationType: HttpAuthorizerType.NONE, + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts index 416e9bed973a3..c3ef630abbbb5 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts @@ -3,7 +3,7 @@ import { Construct } from 'constructs'; import { CfnRoute, CfnRouteProps } from '../apigatewayv2.generated'; import { IRoute } from '../common'; import { IHttpApi } from './api'; -import { IHttpRouteAuthorizer } from './authorizer'; +import { HttpAuthorizerType, IHttpRouteAuthorizer } from './authorizer'; import { IHttpRouteIntegration } from './integration'; /** @@ -147,21 +147,27 @@ export class HttpRoute extends Resource implements IHttpRoute { scope: this.httpApi instanceof Construct ? this.httpApi : this, // scope under the API if it's not imported }) : undefined; - let authorizationScopes = authBindResult?.authorizationScopes ?? []; + let authorizationScopes = authBindResult?.authorizationScopes; if (authBindResult && props.authorizationScopes) { authorizationScopes = Array.from(new Set([ - ...authorizationScopes, + ...authorizationScopes ?? [], ...props.authorizationScopes, ])); } + const authorizationType = authBindResult?.authorizationType === HttpAuthorizerType.NONE ? undefined : authBindResult?.authorizationType; + + if (authorizationScopes?.length === 0) { + authorizationScopes = undefined; + } + const routeProps: CfnRouteProps = { apiId: props.httpApi.apiId, routeKey: props.routeKey.key, target: `integrations/${integration.integrationId}`, authorizerId: authBindResult?.authorizerId, - authorizationType: authBindResult?.authorizationType, + authorizationType, authorizationScopes, }; diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts index a8c5f418f7782..348d8dec9aeb4 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -5,7 +5,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import { Duration, Stack } from '@aws-cdk/core'; import { HttpApi, HttpAuthorizer, HttpAuthorizerType, HttpIntegrationType, HttpMethod, HttpRouteAuthorizerBindOptions, HttpRouteAuthorizerConfig, - HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, IHttpRouteAuthorizer, IHttpRouteIntegration, PayloadFormatVersion, + HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, IHttpRouteAuthorizer, IHttpRouteIntegration, HttpNoneAuthorizer, PayloadFormatVersion, } from '../../lib'; describe('HttpApi', () => { @@ -371,6 +371,121 @@ describe('HttpApi', () => { expect(() => api.apiEndpoint).toThrow(/apiEndpoint is not configured/); }); + + + describe('default authorization settings', () => { + test('can add default authorizer', () => { + const stack = new Stack(); + + const authorizer = new DummyAuthorizer(); + + const httpApi = new HttpApi(stack, 'api', { + defaultAuthorizer: authorizer, + defaultAuthorizationScopes: ['read:pets'], + }); + + httpApi.addRoutes({ + path: '/pets', + methods: [HttpMethod.GET], + integration: new DummyRouteIntegration(), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + AuthorizerId: 'auth-1234', + AuthorizationType: 'JWT', + AuthorizationScopes: ['read:pets'], + }); + }); + + test('can add default authorizer, but remove it for a route', () => { + const stack = new Stack(); + const authorizer = new DummyAuthorizer(); + + const httpApi = new HttpApi(stack, 'api', { + defaultAuthorizer: authorizer, + defaultAuthorizationScopes: ['read:pets'], + }); + + httpApi.addRoutes({ + path: '/pets', + methods: [HttpMethod.GET], + integration: new DummyRouteIntegration(), + }); + + httpApi.addRoutes({ + path: '/chickens', + methods: [HttpMethod.GET], + integration: new DummyRouteIntegration(), + authorizer: new HttpNoneAuthorizer(), + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + RouteKey: 'GET /pets', + AuthorizerId: 'auth-1234', + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + RouteKey: 'GET /chickens', + AuthorizerId: ABSENT, + }); + }); + + test('can remove default scopes for a route', () => { + const stack = new Stack(); + + const authorizer = new DummyAuthorizer(); + + const httpApi = new HttpApi(stack, 'api', { + defaultAuthorizer: authorizer, + defaultAuthorizationScopes: ['read:books'], + }); + + httpApi.addRoutes({ + path: '/pets', + methods: [HttpMethod.GET, HttpMethod.PATCH], + integration: new DummyRouteIntegration(), + authorizationScopes: [], + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + AuthorizationScopes: ABSENT, + }); + }); + + test('can override scopes for a route', () => { + const stack = new Stack(); + + const authorizer = new DummyAuthorizer(); + + const httpApi = new HttpApi(stack, 'api', { + defaultAuthorizer: authorizer, + defaultAuthorizationScopes: ['read:pets'], + }); + + httpApi.addRoutes({ + path: '/pets', + methods: [HttpMethod.GET, HttpMethod.PATCH], + integration: new DummyRouteIntegration(), + }); + + httpApi.addRoutes({ + path: '/chickens', + methods: [HttpMethod.GET, HttpMethod.PATCH], + integration: new DummyRouteIntegration(), + authorizationScopes: ['read:chickens'], + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + RouteKey: 'GET /pets', + AuthorizationScopes: ['read:pets'], + }); + + expect(stack).toHaveResource('AWS::ApiGatewayV2::Route', { + RouteKey: 'GET /chickens', + AuthorizationScopes: ['read:chickens'], + }); + }); + }); }); class DummyRouteIntegration implements IHttpRouteIntegration { 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 index 6afe44cfecda5..56d4889af3d55 100644 --- 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 @@ -77,7 +77,6 @@ "Ref": "MyHttpApi8AEAAC21" }, "RouteKey": "ANY /", - "AuthorizationScopes": [], "Target": { "Fn::Join": [ "", From f2934cf64eed93c934afb6469067461b77570d3c Mon Sep 17 00:00:00 2001 From: Darren Date: Wed, 17 Mar 2021 10:37:36 -0500 Subject: [PATCH 30/32] organize tests --- .../@aws-cdk/aws-iot-actions/lib/republish.ts | 2 + packages/@aws-cdk/aws-iot-actions/lib/sns.ts | 10 +- .../@aws-cdk/aws-iot-actions/package.json | 1 + .../aws-iot-actions/test/iot-actions.test.ts | 104 --------------- ...eg.lambda-topic-rule-action.expected.json} | 0 .../integ.lambda-topic-rule-action.ts} | 5 +- .../test/lambda/lambda.test.ts | 106 ++++++++++++++++ ...republish-topic-rule-action.expected.json} | 0 .../integ.republish-topic-rule-action.ts} | 6 +- .../test/republish/republish.test.ts | 63 ++++++++++ .../integ.sns-topic-rule-action.expected.json | 76 +++++++++++ .../test/sns/integ.sns-topic-rule-action.ts | 19 +++ .../aws-iot-actions/test/sns/sns.test.ts | 118 ++++++++++++++++++ 13 files changed, 394 insertions(+), 116 deletions(-) delete mode 100644 packages/@aws-cdk/aws-iot-actions/test/iot-actions.test.ts rename packages/@aws-cdk/aws-iot-actions/test/{integ.topic-rule-lambda-action.expected.json => lambda/integ.lambda-topic-rule-action.expected.json} (100%) rename packages/@aws-cdk/aws-iot-actions/test/{integ.topic-rule-lambda-action.ts => lambda/integ.lambda-topic-rule-action.ts} (87%) create mode 100644 packages/@aws-cdk/aws-iot-actions/test/lambda/lambda.test.ts rename packages/@aws-cdk/aws-iot-actions/test/{integ.topic-rule-republish-action.expected.json => republish/integ.republish-topic-rule-action.expected.json} (100%) rename packages/@aws-cdk/aws-iot-actions/test/{integ.topic-rule-republish-action.ts => republish/integ.republish-topic-rule-action.ts} (75%) create mode 100644 packages/@aws-cdk/aws-iot-actions/test/republish/republish.test.ts create mode 100644 packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-rule-action.expected.json create mode 100644 packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-rule-action.ts create mode 100644 packages/@aws-cdk/aws-iot-actions/test/sns/sns.test.ts diff --git a/packages/@aws-cdk/aws-iot-actions/lib/republish.ts b/packages/@aws-cdk/aws-iot-actions/lib/republish.ts index e06213cc81b32..b9d3c7a236b06 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/republish.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/republish.ts @@ -42,6 +42,8 @@ export class Republish implements iot.ITopicRuleAction { public bind(rule: iot.ITopicRule): iot.TopicRuleActionConfig { const stack = Stack.of(rule); // Allow rule to publish to topic + // TODO: accept topic rule as prop - this.props.topic.topicRuleArn; + // TODO: grantable topic rules - this.props.topic.grantPublish(rule); const role = this.props.role || singletonTopicRuleRole(rule, [new iam.PolicyStatement({ actions: ['iot:Publish'], resources: [ diff --git a/packages/@aws-cdk/aws-iot-actions/lib/sns.ts b/packages/@aws-cdk/aws-iot-actions/lib/sns.ts index 550a874f7bfe7..77ad227e3c331 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/sns.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/sns.ts @@ -50,12 +50,12 @@ export class Sns implements iot.ITopicRuleAction { constructor(private readonly props: SnsProps) { } - public bind(rule: iot.ITopicRule): iot.TopicRuleActionConfig { + public bind(_rule: iot.ITopicRule): iot.TopicRuleActionConfig { // Allow rule to publish to topic - const role = this.props.role || singletonTopicRuleRole(rule, [new iam.PolicyStatement({ - actions: ['sns:Publish'], - resources: [this.props.topic.topicArn], - })]); + const grantable = this.props.role ? this.props.role : new iam.ServicePrincipal('iot.amazonaws.com'); + this.props.topic.grantPublish(grantable); + + const role = this.props.role ? this.props.role : singletonTopicRuleRole(_rule, []); return { sns: { diff --git a/packages/@aws-cdk/aws-iot-actions/package.json b/packages/@aws-cdk/aws-iot-actions/package.json index f1b2515dbbd09..af0cd775f83e7 100644 --- a/packages/@aws-cdk/aws-iot-actions/package.json +++ b/packages/@aws-cdk/aws-iot-actions/package.json @@ -77,6 +77,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", + "constructs": "^3.2.0", "pkglint": "0.0.0", "nodeunit-shim": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-iot-actions/test/iot-actions.test.ts b/packages/@aws-cdk/aws-iot-actions/test/iot-actions.test.ts deleted file mode 100644 index f8e497ace9831..0000000000000 --- a/packages/@aws-cdk/aws-iot-actions/test/iot-actions.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import '@aws-cdk/assert/jest'; -import * as iot from '@aws-cdk/aws-iot'; -import * as lambda from '@aws-cdk/aws-lambda'; -import * as sns from '@aws-cdk/aws-sns'; -import { Stack } from '@aws-cdk/core'; -import * as actions from '../lib'; - -let stack: Stack; -let rule: iot.TopicRule; - -test('add lambda action', () => { - stack = new Stack(); - rule = new iot.TopicRule(stack, 'TopicRule', { - sql: 'SELECT * FROM \'topic/subtopic\'', - }); - const fn = new lambda.Function(stack, 'Function', { - code: lambda.Code.fromInline('boom'), - handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_8_10, - }); - - rule.addAction(new actions.Lambda({ - function: fn, - })); - - expect(stack).toHaveResourceLike('AWS::IoT::TopicRule', { - TopicRulePayload: { - Actions: [ - { - Lambda: { FunctionArn: { 'Fn::GetAtt': ['Function76856677', 'Arn'] } }, - }, - ], - RuleDisabled: false, - Sql: 'SELECT * FROM \'topic/subtopic\'', - }, - }); - - expect(stack).toHaveResourceLike('AWS::Lambda::Permission', { - Action: 'lambda:InvokeFunction', - FunctionName: { - 'Fn::GetAtt': [ - 'Function76856677', - 'Arn', - ], - }, - Principal: 'iot.amazonaws.com', - SourceAccount: { - Ref: 'AWS::AccountId', - }, - }); -}); -test('add sns action', () => { - stack = new Stack(); - rule = new iot.TopicRule(stack, 'PublishSnsRule', { - sql: 'SELECT * FROM \'topic/subtopic\'', - }); - const topic = new sns.Topic(stack, 'MyTopic'); - - rule.addAction(new actions.Sns({ - topic: topic, - })); - - expect(stack).toHaveResourceLike('AWS::IoT::TopicRule', { - TopicRulePayload: { - Actions: [ - { - Sns: { - MessageFormat: 'RAW', - RoleArn: { 'Fn::GetAtt': ['PublishSnsRuleAllowIot34A25A9A', 'Arn'] }, - TargetArn: { Ref: 'MyTopic86869434' }, - }, - }, - ], - RuleDisabled: false, - Sql: 'SELECT * FROM \'topic/subtopic\'', - }, - }); -}); -test('add republish action', () => { - stack = new Stack(); - rule = new iot.TopicRule(stack, 'RepublishRule', { - sql: 'SELECT * FROM \'topic/subtopic\'', - }); - - rule.addAction(new actions.Republish({ - topic: '$$aws/things/MyThing/shadow/update', - })); - - expect(stack).toHaveResourceLike('AWS::IoT::TopicRule', { - TopicRulePayload: { - Actions: [ - { - Republish: { - Qos: 0, - RoleArn: { 'Fn::GetAtt': ['RepublishRuleAllowIotB39A8B3C', 'Arn'] }, - Topic: '$$aws/things/MyThing/shadow/update', - }, - }, - ], - RuleDisabled: false, - Sql: 'SELECT * FROM \'topic/subtopic\'', - }, - }); -}); diff --git a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-topic-rule-action.expected.json similarity index 100% rename from packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.expected.json rename to packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-topic-rule-action.expected.json diff --git a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.ts b/packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-topic-rule-action.ts similarity index 87% rename from packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.ts rename to packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-topic-rule-action.ts index 0074f9aa98a59..05dff565ea1c6 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-lambda-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-topic-rule-action.ts @@ -2,8 +2,7 @@ import * as iot from '@aws-cdk/aws-iot'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; -import * as actions from '../lib'; - +import * as actions from '../../lib'; // -------------------------------- // Define a rule that triggers an Lambda funcion when data is received. @@ -11,7 +10,7 @@ import * as actions from '../lib'; // const app = new cdk.App(); -const stack = new cdk.Stack(app, 'aws-cdk-topic-rule-lambda-action'); +const stack = new cdk.Stack(app, 'aws-cdk-lambda-topic-rule-action'); // Create an IoT topic rule with an error action. new iot.TopicRule(stack, 'MyIotTopicRule', { diff --git a/packages/@aws-cdk/aws-iot-actions/test/lambda/lambda.test.ts b/packages/@aws-cdk/aws-iot-actions/test/lambda/lambda.test.ts new file mode 100644 index 0000000000000..724e156347c7f --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/lambda/lambda.test.ts @@ -0,0 +1,106 @@ +import '@aws-cdk/assert/jest'; +import * as iot from '@aws-cdk/aws-iot'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as actions from '../../lib'; + +test('add lambda action', () => { + const stack = new Stack(); + const rule = new iot.TopicRule(stack, 'TopicRule', { + sql: 'SELECT * FROM \'topic/subtopic\'', + }); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('boom'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_8_10, + }); + + rule.addAction(new actions.Lambda({ + function: fn, + })); + + expect(stack).toHaveResourceLike('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Lambda: { FunctionArn: { 'Fn::GetAtt': ['Function76856677', 'Arn'] } }, + }, + ], + RuleDisabled: false, + Sql: 'SELECT * FROM \'topic/subtopic\'', + }, + }); + + expect(stack).toHaveResourceLike('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn', + ], + }, + Principal: 'iot.amazonaws.com', + SourceAccount: { + Ref: 'AWS::AccountId', + }, + }); +}); +test('adding different lambda functions as target mutiple times creates multiple permissions', () => { + // GIVEN + const stack = new Stack(); + const fn1 = newTestLambda(stack); + const fn2 = newTestLambda(stack, '2'); + const rule = new iot.TopicRule(stack, 'Rule', { + sql: 'SELECT', + }); + + // WHEN + rule.addAction(new actions.Lambda({ function: fn1 })); + rule.addAction(new actions.Lambda({ function: fn2 })); + + // THEN + expect(stack).toCountResources('AWS::Lambda::Permission', 2); +}); +test('adding same lambda function as target mutiple times creates permission only once', () => { + // GIVEN + const stack = new Stack(); + const fn = newTestLambda(stack); + const rule = new iot.TopicRule(stack, 'Rule', { + sql: 'SELECT', + }); + + // WHEN + rule.addAction(new actions.Lambda({ function: fn })); + rule.addAction(new actions.Lambda({ function: fn })); + + // THEN + expect(stack).toCountResources('AWS::Lambda::Permission', 1); +}); +test('adding same singleton lambda function as target mutiple times creates permission only once', () => { + // GIVEN + const stack = new Stack(); + const fn = new lambda.SingletonFunction(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'bar', + runtime: lambda.Runtime.PYTHON_2_7, + uuid: 'uuid', + }); + + const rule = new iot.TopicRule(stack, 'Rule', { + sql: 'SELECT', + }); + + // WHEN + rule.addAction(new actions.Lambda({ function: fn })); + rule.addAction(new actions.Lambda({ function: fn })); + // THEN + expect(stack).toCountResources('AWS::Lambda::Permission', 1); +}); +function newTestLambda(scope: Construct, suffix = '') { + return new lambda.Function(scope, `MyLambda${suffix}`, { + code: new lambda.InlineCode('foo'), + handler: 'bar', + runtime: lambda.Runtime.PYTHON_2_7, + }); +} diff --git a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/republish/integ.republish-topic-rule-action.expected.json similarity index 100% rename from packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.expected.json rename to packages/@aws-cdk/aws-iot-actions/test/republish/integ.republish-topic-rule-action.expected.json diff --git a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.ts b/packages/@aws-cdk/aws-iot-actions/test/republish/integ.republish-topic-rule-action.ts similarity index 75% rename from packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.ts rename to packages/@aws-cdk/aws-iot-actions/test/republish/integ.republish-topic-rule-action.ts index 5f95e2eebe683..12fe893055657 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/integ.topic-rule-republish-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/test/republish/integ.republish-topic-rule-action.ts @@ -1,7 +1,6 @@ import * as iot from '@aws-cdk/aws-iot'; import * as cdk from '@aws-cdk/core'; -import * as actions from '../lib'; - +import * as actions from '../../lib'; // -------------------------------- // Define a rule that triggers to republish received data. @@ -9,9 +8,8 @@ import * as actions from '../lib'; // const app = new cdk.App(); -const stack = new cdk.Stack(app, 'aws-cdk-topic-rule-republish-action'); +const stack = new cdk.Stack(app, 'aws-cdk-republish-topic-rule-action'); -// Create an IoT topic rule with an error action. new iot.TopicRule(stack, 'MyRepublishTopicRule', { sql: 'SELECT * FROM \'topic/subtopic\'', actions: [ diff --git a/packages/@aws-cdk/aws-iot-actions/test/republish/republish.test.ts b/packages/@aws-cdk/aws-iot-actions/test/republish/republish.test.ts new file mode 100644 index 0000000000000..df710260d1ba6 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/republish/republish.test.ts @@ -0,0 +1,63 @@ +import '@aws-cdk/assert/jest'; +import * as iot from '@aws-cdk/aws-iot'; +import { Stack } from '@aws-cdk/core'; +import * as actions from '../../lib'; + +let stack: Stack; +let rule: iot.TopicRule; + +test('add republish action', () => { + stack = new Stack(); + rule = new iot.TopicRule(stack, 'RepublishRule', { + sql: 'SELECT * FROM \'topic/subtopic\'', + }); + + rule.addAction(new actions.Republish({ + topic: '$$aws/things/MyThing/shadow/update', + })); + + expect(stack).toHaveResourceLike('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Republish: { + Qos: 0, + RoleArn: { 'Fn::GetAtt': ['RepublishRuleAllowIotB39A8B3C', 'Arn'] }, + Topic: '$$aws/things/MyThing/shadow/update', + }, + }, + ], + RuleDisabled: false, + Sql: 'SELECT * FROM \'topic/subtopic\'', + }, + }); + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'iot:Publish', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iot:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':topic/$$aws/things/MyThing/shadow/update', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'RepublishRuleAllowIotDefaultPolicy3B0C81B5', + Roles: [ + { Ref: 'RepublishRuleAllowIotB39A8B3C' }, + ], + }); +}); diff --git a/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-rule-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-rule-action.expected.json new file mode 100644 index 0000000000000..97ab8db500d3b --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-rule-action.expected.json @@ -0,0 +1,76 @@ +{ + "Resources": { + "MyTopic86869434": { + "Type": "AWS::SNS::Topic" + }, + "MyTopicPolicy12A5EC17": { + "Type": "AWS::SNS::TopicPolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + }, + "Resource": { + "Ref": "MyTopic86869434" + }, + "Sid": "0" + } + ], + "Version": "2012-10-17" + }, + "Topics": [ + { + "Ref": "MyTopic86869434" + } + ] + } + }, + "My84E10354": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Sns": { + "MessageFormat": "RAW", + "RoleArn": { + "Fn::GetAtt": [ + "MyAllowIotE8084C57", + "Arn" + ] + }, + "TargetArn": { + "Ref": "MyTopic86869434" + } + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'topic/subtopic'" + } + } + }, + "MyAllowIotE8084C57": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-rule-action.ts b/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-rule-action.ts new file mode 100644 index 0000000000000..be6245de6a2c2 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-rule-action.ts @@ -0,0 +1,19 @@ +import * as iot from '@aws-cdk/aws-iot'; +import * as sns from '@aws-cdk/aws-sns'; + +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-topic-rule-sns-action'); + +const topic = new sns.Topic(stack, 'MyTopic'); + +const rule = new iot.TopicRule(stack, 'My', { + sql: 'SELECT * FROM \'topic/subtopic\'', +}); + +rule.addAction(new actions.Sns({ topic: topic })); + +app.synth(); diff --git a/packages/@aws-cdk/aws-iot-actions/test/sns/sns.test.ts b/packages/@aws-cdk/aws-iot-actions/test/sns/sns.test.ts new file mode 100644 index 0000000000000..46a2bb3f6af81 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/sns/sns.test.ts @@ -0,0 +1,118 @@ +import { expect, haveResource, countResources } from '@aws-cdk/assert'; +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as sns from '@aws-cdk/aws-sns'; +import { Stack } from '@aws-cdk/core'; +import * as actions from '../../lib'; + +test('sns topic as a rule action', () => { + // GIVEN + const stack = new Stack(); + const topic = new sns.Topic(stack, 'MyTopic'); + const rule = new iot.TopicRule(stack, 'MyRule', { + sql: 'SELECT', + }); + + // WHEN + rule.addAction(new actions.Sns({ topic })); + + // THEN + expect(stack).to(haveResource('AWS::SNS::TopicPolicy', { + PolicyDocument: { + Statement: [ + { + Sid: '0', + Action: 'sns:Publish', + Effect: 'Allow', + Principal: { Service: 'iot.amazonaws.com' }, + Resource: { Ref: 'MyTopic86869434' }, + }, + ], + Version: '2012-10-17', + }, + Topics: [{ Ref: 'MyTopic86869434' }], + })); + expect(stack).to(haveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Sns: { + MessageFormat: 'RAW', + RoleArn: { + 'Fn::GetAtt': ['MyRuleAllowIot33481905', 'Arn'], + }, + TargetArn: { Ref: 'MyTopic86869434' }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: 'SELECT', + }, + })); +}); + +test('sns topic with role as a rule action', () => { + // GIVEN + const stack = new Stack(); + const topic = new sns.Topic(stack, 'MyTopic'); + const role = new iam.Role(stack, 'MyCustomRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const rule = new iot.TopicRule(stack, 'MyRule', { + sql: 'SELECT', + }); + + // WHEN + rule.addAction(new actions.Sns({ topic, role })); + + expect(stack).to(haveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Sns: { + MessageFormat: 'RAW', + RoleArn: { + 'Fn::GetAtt': ['MyCustomRoleC8C89DCB', 'Arn'], + }, + TargetArn: { Ref: 'MyTopic86869434' }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: 'SELECT', + }, + })); +}); +test('multiple uses of a topic as a target results in a single policy statement', () => { + // GIVEN + const stack = new Stack(); + const topic = new sns.Topic(stack, 'MyTopic'); + + //WHEN + for (let i = 0; i < 5; ++i) { + const rule = new iot.TopicRule(stack, `Rule${i}`, { + sql: 'SELECT', + }); + rule.addAction(new actions.Sns({ topic })); + } + + // THEN + expect(stack).to(countResources('AWS::SNS::TopicPolicy', 1)); + expect(stack).to(haveResource('AWS::SNS::TopicPolicy', { + PolicyDocument: { + Statement: [ + { + Sid: '0', + Action: 'sns:Publish', + Effect: 'Allow', + Principal: { Service: 'iot.amazonaws.com' }, + Resource: { Ref: 'MyTopic86869434' }, + }, + ], + Version: '2012-10-17', + }, + Topics: [{ Ref: 'MyTopic86869434' }], + })); +}); From 8c5840e6bf728cc22444890e079835ebd6a91b88 Mon Sep 17 00:00:00 2001 From: Darren Date: Wed, 17 Mar 2021 11:54:40 -0500 Subject: [PATCH 31/32] add queue and fix action call signature --- .../@aws-cdk/aws-iot-actions/lib/index.ts | 1 + .../@aws-cdk/aws-iot-actions/lib/lambda.ts | 22 +-- packages/@aws-cdk/aws-iot-actions/lib/sns.ts | 14 +- packages/@aws-cdk/aws-iot-actions/lib/sqs.ts | 46 +++++++ .../@aws-cdk/aws-iot-actions/package.json | 6 +- .../lambda/integ.lambda-topic-rule-action.ts | 29 ++-- .../test/lambda/lambda.test.ts | 17 ++- .../test/sns/integ.sns-topic-rule-action.ts | 2 +- .../aws-iot-actions/test/sns/sns.test.ts | 6 +- .../integ.sqs-topic-rule-action.expected.json | 84 ++++++++++++ .../test/sqs/integ.sqs-topic-rule-action.ts | 19 +++ .../aws-iot-actions/test/sqs/sqs.test.ts | 128 ++++++++++++++++++ 12 files changed, 319 insertions(+), 55 deletions(-) create mode 100644 packages/@aws-cdk/aws-iot-actions/lib/sqs.ts create mode 100644 packages/@aws-cdk/aws-iot-actions/test/sqs/integ.sqs-topic-rule-action.expected.json create mode 100644 packages/@aws-cdk/aws-iot-actions/test/sqs/integ.sqs-topic-rule-action.ts create mode 100644 packages/@aws-cdk/aws-iot-actions/test/sqs/sqs.test.ts diff --git a/packages/@aws-cdk/aws-iot-actions/lib/index.ts b/packages/@aws-cdk/aws-iot-actions/lib/index.ts index 5621e67ba8700..0e44798488793 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/index.ts @@ -1,3 +1,4 @@ export * from './lambda'; export * from './republish'; export * from './sns'; +export * from './sqs'; diff --git a/packages/@aws-cdk/aws-iot-actions/lib/lambda.ts b/packages/@aws-cdk/aws-iot-actions/lib/lambda.ts index 2de0229e54306..7f514c5f88f16 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/lambda.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/lambda.ts @@ -3,28 +3,18 @@ import * as iot from '@aws-cdk/aws-iot'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; -/** - * Construction properties for a Lambda action. - */ -export interface LambdaProps { - /** - * The Lambda function to invoke - */ - readonly function: lambda.IFunction; -} - /** * Calls an AWS Lambda function */ -export class Lambda implements iot.ITopicRuleAction { - constructor(private readonly props: LambdaProps) { +export class LambdaFunction implements iot.ITopicRuleAction { + constructor(private readonly handler: lambda.IFunction) { } public bind(rule: iot.ITopicRule): iot.TopicRuleActionConfig { // Allow rule to invoke lambda function const permissionId = 'AllowIot'; - if (!this.props.function.permissionsNode.tryFindChild(permissionId)) { - this.props.function.addPermission(permissionId, { + if (!this.handler.permissionsNode.tryFindChild(permissionId)) { + this.handler.addPermission(permissionId, { action: 'lambda:InvokeFunction', principal: new iam.ServicePrincipal('iot.amazonaws.com'), sourceAccount: cdk.Aws.ACCOUNT_ID, @@ -32,7 +22,7 @@ export class Lambda implements iot.ITopicRuleAction { } // Ensure permission is deployed before rule - const permission = this.props.function.permissionsNode.tryFindChild(permissionId) as lambda.CfnPermission; + const permission = this.handler.permissionsNode.tryFindChild(permissionId) as lambda.CfnPermission; if (permission) { rule.node.addDependency(permission); } else { @@ -42,7 +32,7 @@ export class Lambda implements iot.ITopicRuleAction { return { lambda: { - functionArn: this.props.function.functionArn, + functionArn: this.handler.functionArn, }, }; } diff --git a/packages/@aws-cdk/aws-iot-actions/lib/sns.ts b/packages/@aws-cdk/aws-iot-actions/lib/sns.ts index 77ad227e3c331..9616fbc8b148d 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/sns.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/sns.ts @@ -19,11 +19,7 @@ export enum MessageFormats { /** * Construction properties for a sns publish action. */ -export interface SnsProps { - /** - * The Topic to publish on - */ - readonly topic: sns.ITopic; +export interface SnsTopicProps { /** * (Optional) The message format of the message to publish. Accepted values * are "JSON" and "RAW". The default value of the attribute is "RAW". SNS uses @@ -46,21 +42,21 @@ export interface SnsProps { /** * Publishes to a Topic */ -export class Sns implements iot.ITopicRuleAction { - constructor(private readonly props: SnsProps) { +export class SnsTopic implements iot.ITopicRuleAction { + constructor(private readonly topic: sns.ITopic, private readonly props: SnsTopicProps = {}) { } public bind(_rule: iot.ITopicRule): iot.TopicRuleActionConfig { // Allow rule to publish to topic const grantable = this.props.role ? this.props.role : new iam.ServicePrincipal('iot.amazonaws.com'); - this.props.topic.grantPublish(grantable); + this.topic.grantPublish(grantable); const role = this.props.role ? this.props.role : singletonTopicRuleRole(_rule, []); return { sns: { messageFormat: this.props.messageFormat || MessageFormats.RAW, - targetArn: this.props.topic.topicArn, + targetArn: this.topic.topicArn, roleArn: role.roleArn, }, }; diff --git a/packages/@aws-cdk/aws-iot-actions/lib/sqs.ts b/packages/@aws-cdk/aws-iot-actions/lib/sqs.ts new file mode 100644 index 0000000000000..147e0106f6e61 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/sqs.ts @@ -0,0 +1,46 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as sqs from '@aws-cdk/aws-sqs'; +import { singletonTopicRuleRole } from './util'; + +/** + * Construction properties for a sqs send message action. + */ +export interface SqsQueueProps { + /** + * Specifies whether to use Base64 encoding. + * + * @default - false + */ + readonly useBase64?: boolean; + /** + * The IAM role that grants access. + * + * @default - a role will be created + */ + readonly role?: iam.IRole; +} + +/** + * Publishes to a Queue + */ +export class SqsQueue implements iot.ITopicRuleAction { + constructor(private readonly queue: sqs.IQueue, private readonly props: SqsQueueProps = {}) { + } + + public bind(_rule: iot.ITopicRule): iot.TopicRuleActionConfig { + // Allow rule to publish to topic + const grantable = this.props.role ? this.props.role : new iam.ServicePrincipal('iot.amazonaws.com'); + this.queue.grantSendMessages(grantable); + + const role = this.props.role ? this.props.role : singletonTopicRuleRole(_rule, []); + + return { + sqs: { + useBase64: this.props.useBase64 || false, + queueUrl: this.queue.queueUrl, + roleArn: role.roleArn, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot-actions/package.json b/packages/@aws-cdk/aws-iot-actions/package.json index af0cd775f83e7..dd360ab16a22e 100644 --- a/packages/@aws-cdk/aws-iot-actions/package.json +++ b/packages/@aws-cdk/aws-iot-actions/package.json @@ -86,7 +86,8 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", - "@aws-cdk/aws-sns": "0.0.0" + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/aws-sqs": "0.0.0" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { @@ -94,7 +95,8 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", - "@aws-cdk/aws-sns": "0.0.0" + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/aws-sqs": "0.0.0" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" diff --git a/packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-topic-rule-action.ts b/packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-topic-rule-action.ts index 05dff565ea1c6..8bb3ca39ebbdf 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-topic-rule-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/test/lambda/integ.lambda-topic-rule-action.ts @@ -4,26 +4,25 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import * as actions from '../../lib'; -// -------------------------------- -// Define a rule that triggers an Lambda funcion when data is received. -// Automatically creates invoke lambda permission -// +/** + * Define a rule that triggers a Lambda function when data is received. + * + * Automatically creates invoke permission + */ const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-cdk-lambda-topic-rule-action'); -// Create an IoT topic rule with an error action. -new iot.TopicRule(stack, 'MyIotTopicRule', { +const func = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('boom'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_12_X, +}); + +const rule = new iot.TopicRule(stack, 'MyIotTopicRule', { sql: 'SELECT * FROM \'topic/subtopic\'', - actions: [ - new actions.Lambda({ - function: new lambda.Function(stack, 'Function', { - code: lambda.Code.fromInline('boom'), - handler: 'index.handler', - runtime: lambda.Runtime.NODEJS_12_X, - }), - }), - ], }); +rule.addAction(new actions.LambdaFunction(func)); + app.synth(); diff --git a/packages/@aws-cdk/aws-iot-actions/test/lambda/lambda.test.ts b/packages/@aws-cdk/aws-iot-actions/test/lambda/lambda.test.ts index 724e156347c7f..a326db57fb710 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/lambda/lambda.test.ts +++ b/packages/@aws-cdk/aws-iot-actions/test/lambda/lambda.test.ts @@ -16,9 +16,7 @@ test('add lambda action', () => { runtime: lambda.Runtime.NODEJS_8_10, }); - rule.addAction(new actions.Lambda({ - function: fn, - })); + rule.addAction(new actions.LambdaFunction(fn)); expect(stack).toHaveResourceLike('AWS::IoT::TopicRule', { TopicRulePayload: { @@ -56,8 +54,8 @@ test('adding different lambda functions as target mutiple times creates multiple }); // WHEN - rule.addAction(new actions.Lambda({ function: fn1 })); - rule.addAction(new actions.Lambda({ function: fn2 })); + rule.addAction(new actions.LambdaFunction(fn1)); + rule.addAction(new actions.LambdaFunction(fn2)); // THEN expect(stack).toCountResources('AWS::Lambda::Permission', 2); @@ -71,8 +69,8 @@ test('adding same lambda function as target mutiple times creates permission onl }); // WHEN - rule.addAction(new actions.Lambda({ function: fn })); - rule.addAction(new actions.Lambda({ function: fn })); + rule.addAction(new actions.LambdaFunction(fn)); + rule.addAction(new actions.LambdaFunction(fn)); // THEN expect(stack).toCountResources('AWS::Lambda::Permission', 1); @@ -92,8 +90,9 @@ test('adding same singleton lambda function as target mutiple times creates perm }); // WHEN - rule.addAction(new actions.Lambda({ function: fn })); - rule.addAction(new actions.Lambda({ function: fn })); + rule.addAction(new actions.LambdaFunction(fn)); + rule.addAction(new actions.LambdaFunction(fn)); + // THEN expect(stack).toCountResources('AWS::Lambda::Permission', 1); }); diff --git a/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-rule-action.ts b/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-rule-action.ts index be6245de6a2c2..06718ddf7b6ce 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-rule-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/test/sns/integ.sns-topic-rule-action.ts @@ -14,6 +14,6 @@ const rule = new iot.TopicRule(stack, 'My', { sql: 'SELECT * FROM \'topic/subtopic\'', }); -rule.addAction(new actions.Sns({ topic: topic })); +rule.addAction(new actions.SnsTopic(topic)); app.synth(); diff --git a/packages/@aws-cdk/aws-iot-actions/test/sns/sns.test.ts b/packages/@aws-cdk/aws-iot-actions/test/sns/sns.test.ts index 46a2bb3f6af81..d0ba5641e80d8 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/sns/sns.test.ts +++ b/packages/@aws-cdk/aws-iot-actions/test/sns/sns.test.ts @@ -14,7 +14,7 @@ test('sns topic as a rule action', () => { }); // WHEN - rule.addAction(new actions.Sns({ topic })); + rule.addAction(new actions.SnsTopic(topic)); // THEN expect(stack).to(haveResource('AWS::SNS::TopicPolicy', { @@ -64,7 +64,7 @@ test('sns topic with role as a rule action', () => { }); // WHEN - rule.addAction(new actions.Sns({ topic, role })); + rule.addAction(new actions.SnsTopic(topic, { role: role })); expect(stack).to(haveResource('AWS::IoT::TopicRule', { TopicRulePayload: { @@ -95,7 +95,7 @@ test('multiple uses of a topic as a target results in a single policy statement' const rule = new iot.TopicRule(stack, `Rule${i}`, { sql: 'SELECT', }); - rule.addAction(new actions.Sns({ topic })); + rule.addAction(new actions.SnsTopic(topic)); } // THEN diff --git a/packages/@aws-cdk/aws-iot-actions/test/sqs/integ.sqs-topic-rule-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/sqs/integ.sqs-topic-rule-action.expected.json new file mode 100644 index 0000000000000..6cb06af4a94a2 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/sqs/integ.sqs-topic-rule-action.expected.json @@ -0,0 +1,84 @@ +{ + "Resources": { + "MyQueueE6CA6235": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "MyQueuePolicy6BBEDDAC": { + "Type": "AWS::SQS::QueuePolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl" + ], + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "MyQueueE6CA6235", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "Queues": [ + { + "Ref": "MyQueueE6CA6235" + } + ] + } + }, + "My84E10354": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Sqs": { + "QueueUrl": { + "Ref": "MyQueueE6CA6235" + }, + "RoleArn": { + "Fn::GetAtt": [ + "MyAllowIotE8084C57", + "Arn" + ] + }, + "UseBase64": false + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'topic/subtopic'" + } + } + }, + "MyAllowIotE8084C57": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/sqs/integ.sqs-topic-rule-action.ts b/packages/@aws-cdk/aws-iot-actions/test/sqs/integ.sqs-topic-rule-action.ts new file mode 100644 index 0000000000000..bf3c085ea53a4 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/sqs/integ.sqs-topic-rule-action.ts @@ -0,0 +1,19 @@ +import * as iot from '@aws-cdk/aws-iot'; +import * as sqs from '@aws-cdk/aws-sqs'; + +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-topic-rule-sns-action'); + +const queue = new sqs.Queue(stack, 'MyQueue'); + +const rule = new iot.TopicRule(stack, 'My', { + sql: 'SELECT * FROM \'topic/subtopic\'', +}); + +rule.addAction(new actions.SqsQueue(queue)); + +app.synth(); diff --git a/packages/@aws-cdk/aws-iot-actions/test/sqs/sqs.test.ts b/packages/@aws-cdk/aws-iot-actions/test/sqs/sqs.test.ts new file mode 100644 index 0000000000000..1bac6e3e698c9 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/sqs/sqs.test.ts @@ -0,0 +1,128 @@ +import { expect, haveResource, countResources } from '@aws-cdk/assert'; +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as sqs from '@aws-cdk/aws-sqs'; +import { Stack } from '@aws-cdk/core'; +import * as actions from '../../lib'; + +test('sns topic as a rule action', () => { + // GIVEN + const stack = new Stack(); + const queue = new sqs.Queue(stack, 'MyQueue'); + const rule = new iot.TopicRule(stack, 'MyRule', { + sql: 'SELECT', + }); + + // WHEN + rule.addAction(new actions.SqsQueue(queue)); + + // THEN + expect(stack).to(haveResource('AWS::SQS::QueuePolicy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'sqs:SendMessage', + 'sqs:GetQueueAttributes', + 'sqs:GetQueueUrl', + ], + Effect: 'Allow', + Principal: { Service: 'iot.amazonaws.com' }, + Resource: { + 'Fn::GetAtt': ['MyQueueE6CA6235', 'Arn'], + }, + }, + ], + Version: '2012-10-17', + }, + Queues: [{ Ref: 'MyQueueE6CA6235' }], + })); + expect(stack).to(haveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Sqs: { + QueueUrl: { Ref: 'MyQueueE6CA6235' }, + RoleArn: { + 'Fn::GetAtt': ['MyRuleAllowIot33481905', 'Arn'], + }, + UseBase64: false, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: 'SELECT', + }, + })); +}); + +test('sqs topic with role as a rule action', () => { + // GIVEN + const stack = new Stack(); + const queue = new sqs.Queue(stack, 'MyQueue'); + const role = new iam.Role(stack, 'MyCustomRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const rule = new iot.TopicRule(stack, 'MyRule', { + sql: 'SELECT', + }); + + // WHEN + rule.addAction(new actions.SqsQueue(queue, { role: role })); + + expect(stack).to(haveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Sqs: { + QueueUrl: { Ref: 'MyQueueE6CA6235' }, + RoleArn: { + 'Fn::GetAtt': ['MyCustomRoleC8C89DCB', 'Arn'], + }, + UseBase64: false, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: 'SELECT', + }, + })); +}); +test('multiple uses of a queue as an action results in a single policy statement', () => { + // GIVEN + const stack = new Stack(); + const queue = new sqs.Queue(stack, 'MyQueue'); + + //WHEN + for (let i = 0; i < 5; ++i) { + const rule = new iot.TopicRule(stack, `Rule${i}`, { + sql: 'SELECT', + }); + rule.addAction(new actions.SqsQueue(queue)); + } + + // THEN + expect(stack).to(countResources('AWS::SQS::QueuePolicy', 1)); + expect(stack).to(haveResource('AWS::SQS::QueuePolicy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'sqs:SendMessage', + 'sqs:GetQueueAttributes', + 'sqs:GetQueueUrl', + ], + Effect: 'Allow', + Principal: { Service: 'iot.amazonaws.com' }, + Resource: { + 'Fn::GetAtt': ['MyQueueE6CA6235', 'Arn'], + }, + }, + ], + Version: '2012-10-17', + }, + Queues: [{ Ref: 'MyQueueE6CA6235' }], + })); +}); From 486be6c8ebc8df5a932880bacdcd1f762d33304c Mon Sep 17 00:00:00 2001 From: Darren Date: Wed, 17 Mar 2021 15:56:08 -0500 Subject: [PATCH 32/32] topic rule grant publish --- .../@aws-cdk/aws-iot-actions/lib/republish.ts | 35 +-- .../test/integ.kit-and-caboodle.expected.json | 239 ++++++++++++++++++ .../test/integ.kit-and-caboodle.ts | 27 ++ ....republish-topic-rule-action.expected.json | 78 +++++- .../integ.republish-topic-rule-action.ts | 22 +- .../test/republish/republish.test.ts | 14 +- packages/@aws-cdk/aws-iot/lib/topic-rule.ts | 45 +++- packages/@aws-cdk/aws-iot/lib/util.ts | 39 ++- packages/@aws-cdk/aws-iot/package.json | 3 + packages/@aws-cdk/aws-iot/test/iot.test.ts | 6 - .../@aws-cdk/aws-iot/test/topic-rule.test.ts | 65 ++++- 11 files changed, 514 insertions(+), 59 deletions(-) create mode 100644 packages/@aws-cdk/aws-iot-actions/test/integ.kit-and-caboodle.expected.json create mode 100644 packages/@aws-cdk/aws-iot-actions/test/integ.kit-and-caboodle.ts delete mode 100644 packages/@aws-cdk/aws-iot/test/iot.test.ts diff --git a/packages/@aws-cdk/aws-iot-actions/lib/republish.ts b/packages/@aws-cdk/aws-iot-actions/lib/republish.ts index b9d3c7a236b06..78a526e92b7ef 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/republish.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/republish.ts @@ -1,29 +1,21 @@ import * as iam from '@aws-cdk/aws-iam'; import * as iot from '@aws-cdk/aws-iot'; -import { Stack, Arn } from '@aws-cdk/core'; import { singletonTopicRuleRole } from './util'; /** * Construction properties for a Republish action. */ export interface RepublishProps { + /** + * The name of the MQTT topic + */ + readonly topic: string; /** * The Quality of Service (Qos) level to use when republishing messages. * * @default - 0 */ readonly qos?: number; - /** - * - * The MQTT topic to which to republish the message. - * - * To republish to a reserved topic, which begins with `$`, use `$$` instead. - * - * For example, to republish to the device shadow topic - * `$aws/things/MyThing/shadow/update`, specify the topic as - * `$$aws/things/MyThing/shadow/update`. - */ - readonly topic: string; /** * The IAM role that grants access. * @@ -35,25 +27,14 @@ export interface RepublishProps { /** * Publishes to a IoT Topic */ -export class Republish implements iot.ITopicRuleAction { - constructor(private readonly props: RepublishProps) { +export class RepublishTopic implements iot.ITopicRuleAction { + constructor(private readonly topic: iot.ITopicRule, private readonly props: RepublishProps) { } public bind(rule: iot.ITopicRule): iot.TopicRuleActionConfig { - const stack = Stack.of(rule); // Allow rule to publish to topic - // TODO: accept topic rule as prop - this.props.topic.topicRuleArn; - // TODO: grantable topic rules - this.props.topic.grantPublish(rule); - const role = this.props.role || singletonTopicRuleRole(rule, [new iam.PolicyStatement({ - actions: ['iot:Publish'], - resources: [ - Arn.format({ - resource: 'topic', - service: 'iot', - resourceName: this.props.topic, - }, stack), - ], - })]); + const role = this.props.role || singletonTopicRuleRole(rule, []); + this.topic.grantPublish(role, this.props.topic); return { republish: { diff --git a/packages/@aws-cdk/aws-iot-actions/test/integ.kit-and-caboodle.expected.json b/packages/@aws-cdk/aws-iot-actions/test/integ.kit-and-caboodle.expected.json new file mode 100644 index 0000000000000..e8644877e9482 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/integ.kit-and-caboodle.expected.json @@ -0,0 +1,239 @@ +{ + "Resources": { + "ErrorTopicA0904A23": { + "Type": "AWS::SNS::Topic" + }, + "ErrorTopicPolicyA43559E4": { + "Type": "AWS::SNS::TopicPolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + }, + "Resource": { + "Ref": "ErrorTopicA0904A23" + }, + "Sid": "0" + } + ], + "Version": "2012-10-17" + }, + "Topics": [ + { + "Ref": "ErrorTopicA0904A23" + } + ] + } + }, + "MyNotifierTopicAllowIot154F3A47": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "DependsOn": [ + "FunctionAllowIotD2ECA9CD" + ] + }, + "MyNotifierTopicC60F4F3C": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Lambda": { + "FunctionArn": { + "Fn::GetAtt": [ + "Function76856677", + "Arn" + ] + } + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "ErrorAction": { + "Sns": { + "MessageFormat": "RAW", + "RoleArn": { + "Fn::GetAtt": [ + "MyNotifierTopicAllowIot154F3A47", + "Arn" + ] + }, + "TargetArn": { + "Ref": "ErrorTopicA0904A23" + } + } + }, + "RuleDisabled": false, + "Sql": "SELECT teapots FROM 'coffee/shop'" + } + }, + "DependsOn": [ + "FunctionAllowIotD2ECA9CD" + ] + }, + "FunctionServiceRole675BB04A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Function76856677": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "boom" + }, + "Role": { + "Fn::GetAtt": [ + "FunctionServiceRole675BB04A", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "FunctionServiceRole675BB04A" + ] + }, + "FunctionAllowIotD2ECA9CD": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Function76856677", + "Arn" + ] + }, + "Principal": "iot.amazonaws.com", + "SourceAccount": { + "Ref": "AWS::AccountId" + } + } + }, + "MyWorkTopicE967237A": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Republish": { + "Qos": 0, + "RoleArn": { + "Fn::GetAtt": [ + "MyWorkTopicAllowIot06B368E4", + "Arn" + ] + }, + "Topic": "coffee/shop" + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "RuleDisabled": false, + "Sql": "SELECT * FROM 'inventory'" + } + } + }, + "MyWorkTopicAllowIot06B368E4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyWorkTopicAllowIotDefaultPolicy13C30DF0": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "iot:Publish", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iot:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":topic/coffee/shop" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyWorkTopicAllowIotDefaultPolicy13C30DF0", + "Roles": [ + { + "Ref": "MyWorkTopicAllowIot06B368E4" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/integ.kit-and-caboodle.ts b/packages/@aws-cdk/aws-iot-actions/test/integ.kit-and-caboodle.ts new file mode 100644 index 0000000000000..784be670fa529 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/integ.kit-and-caboodle.ts @@ -0,0 +1,27 @@ +import * as iot from '@aws-cdk/aws-iot'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sns from '@aws-cdk/aws-sns'; +import { App, Stack } from '@aws-cdk/core'; +import * as actions from '../lib'; + +const app = new App(); +const stack = new Stack(app, 'MyTopicRule'); + +const rule = new iot.TopicRule(stack, 'MyNotifierTopic', { + sql: 'SELECT teapots FROM \'coffee/shop\'', + errorAction: new actions.SnsTopic(new sns.Topic(stack, 'ErrorTopic')), +}); + +const func = new lambda.Function(stack, 'Function', { + code: lambda.Code.fromInline('boom'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_12_X, +}); + +rule.addAction(new actions.LambdaFunction(func)); + +new iot.TopicRule(stack, 'MyWorkTopic', { + sql: 'SELECT * FROM \'inventory\'', + actions: [new actions.RepublishTopic(rule, { topic: 'coffee/shop' })], +}); + diff --git a/packages/@aws-cdk/aws-iot-actions/test/republish/integ.republish-topic-rule-action.expected.json b/packages/@aws-cdk/aws-iot-actions/test/republish/integ.republish-topic-rule-action.expected.json index 4314409e7716f..5c1414c3df72f 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/republish/integ.republish-topic-rule-action.expected.json +++ b/packages/@aws-cdk/aws-iot-actions/test/republish/integ.republish-topic-rule-action.expected.json @@ -1,5 +1,77 @@ { "Resources": { + "MyTopic86869434": { + "Type": "AWS::SNS::Topic" + }, + "MyTopicPolicy12A5EC17": { + "Type": "AWS::SNS::TopicPolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + }, + "Resource": { + "Ref": "MyTopic86869434" + }, + "Sid": "0" + } + ], + "Version": "2012-10-17" + }, + "Topics": [ + { + "Ref": "MyTopic86869434" + } + ] + } + }, + "DownstreamTopicBE449D24": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Sns": { + "MessageFormat": "RAW", + "RoleArn": { + "Fn::GetAtt": [ + "DownstreamTopicAllowIotB0ED12F8", + "Arn" + ] + }, + "TargetArn": { + "Ref": "MyTopic86869434" + } + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "RuleDisabled": false, + "Sql": "SELECT teapots FROM 'inventory'" + } + } + }, + "DownstreamTopicAllowIotB0ED12F8": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, "MyRepublishTopicRuleD870AE64": { "Type": "AWS::IoT::TopicRule", "Properties": { @@ -14,13 +86,13 @@ "Arn" ] }, - "Topic": "some/topic" + "Topic": "inventory" } } ], "AwsIotSqlVersion": "2015-10-08", "RuleDisabled": false, - "Sql": "SELECT * FROM 'topic/subtopic'" + "Sql": "SELECT teapots FROM 'coffee/shop'" } } }, @@ -65,7 +137,7 @@ { "Ref": "AWS::AccountId" }, - ":topic/some/topic" + ":topic/inventory" ] ] } diff --git a/packages/@aws-cdk/aws-iot-actions/test/republish/integ.republish-topic-rule-action.ts b/packages/@aws-cdk/aws-iot-actions/test/republish/integ.republish-topic-rule-action.ts index 12fe893055657..5a02564a2ab54 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/republish/integ.republish-topic-rule-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/test/republish/integ.republish-topic-rule-action.ts @@ -1,22 +1,24 @@ import * as iot from '@aws-cdk/aws-iot'; +import * as sns from '@aws-cdk/aws-sns'; import * as cdk from '@aws-cdk/core'; import * as actions from '../../lib'; -// -------------------------------- -// Define a rule that triggers to republish received data. -// Automatically creates invoke lambda permission -// +/** +* Define a rule that triggers to republish received data. +* Automatically creates invoke lambda permission +*/ const app = new cdk.App(); const stack = new cdk.Stack(app, 'aws-cdk-republish-topic-rule-action'); +const downstream = new iot.TopicRule(stack, 'DownstreamTopic', { + sql: 'SELECT teapots FROM \'inventory\'', + actions: [new actions.SnsTopic(new sns.Topic(stack, 'MyTopic'))], +}); + new iot.TopicRule(stack, 'MyRepublishTopicRule', { - sql: 'SELECT * FROM \'topic/subtopic\'', - actions: [ - new actions.Republish({ - topic: 'some/topic', - }), - ], + sql: 'SELECT teapots FROM \'coffee/shop\'', + actions: [new actions.RepublishTopic(downstream, { topic: 'inventory' })], }); app.synth(); diff --git a/packages/@aws-cdk/aws-iot-actions/test/republish/republish.test.ts b/packages/@aws-cdk/aws-iot-actions/test/republish/republish.test.ts index df710260d1ba6..ac318344549d8 100644 --- a/packages/@aws-cdk/aws-iot-actions/test/republish/republish.test.ts +++ b/packages/@aws-cdk/aws-iot-actions/test/republish/republish.test.ts @@ -1,5 +1,6 @@ import '@aws-cdk/assert/jest'; import * as iot from '@aws-cdk/aws-iot'; +import * as sns from '@aws-cdk/aws-sns'; import { Stack } from '@aws-cdk/core'; import * as actions from '../../lib'; @@ -12,9 +13,12 @@ test('add republish action', () => { sql: 'SELECT * FROM \'topic/subtopic\'', }); - rule.addAction(new actions.Republish({ - topic: '$$aws/things/MyThing/shadow/update', - })); + const downstream = new iot.TopicRule(stack, 'DownstreamRule', { + sql: 'SELECT teapot FROM \'coffe/shop\'', + actions: [new actions.SnsTopic(new sns.Topic(stack, 'MySnsTopic'))], + }); + + rule.addAction(new actions.RepublishTopic(downstream, { topic: 'coffee/shop' })); expect(stack).toHaveResourceLike('AWS::IoT::TopicRule', { TopicRulePayload: { @@ -23,7 +27,7 @@ test('add republish action', () => { Republish: { Qos: 0, RoleArn: { 'Fn::GetAtt': ['RepublishRuleAllowIotB39A8B3C', 'Arn'] }, - Topic: '$$aws/things/MyThing/shadow/update', + Topic: 'coffee/shop', }, }, ], @@ -47,7 +51,7 @@ test('add republish action', () => { { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, - ':topic/$$aws/things/MyThing/shadow/update', + ':topic/coffee/shop', ], ], }, diff --git a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts index afcdd8bba8c98..45bfab74a6f0c 100644 --- a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts +++ b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts @@ -1,8 +1,9 @@ +import { Grant, IGrantable, PolicyStatement, AddToResourcePolicyResult } from '@aws-cdk/aws-iam'; import { Resource, IResource, Lazy } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnTopicRule } from './iot.generated'; import { ITopicRuleAction } from './topic-rule-action'; -import { parseRuleName, undefinedIfAllValuesAreEmpty } from './util'; +import { singletonTopicRuleRole, parseRuleName, undefinedIfAllValuesAreEmpty, topicArn } from './util'; /** * The AWS IoT rules engine uses an SQL-like syntax to select data from MQTT @@ -48,6 +49,14 @@ export interface ITopicRule extends IResource { * */ readonly topicRuleArn: string; + /** + * Adds a statement to the IAM resource created for this topic rule + */ + addToResourcePolicy(statement: PolicyStatement): AddToResourcePolicyResult; + /** + * Grant topic publishing permissions to the given identity + */ + grantPublish(identity: IGrantable, topic: string): Grant; } /** * A reference to an IoT Topic Rule. @@ -105,10 +114,37 @@ export interface TopicRuleProps { readonly errorAction?: ITopicRuleAction; } +/** + * Either a new or imported topic rule + */ +export abstract class TopicRuleBase extends Resource implements ITopicRule { + public abstract readonly ruleName: string; + public abstract readonly topicRuleArn: string; + + /** + * Add a statement to the IAM resource for this rule + */ + public addToResourcePolicy(statement: PolicyStatement): AddToResourcePolicyResult { + singletonTopicRuleRole(this, [statement]); + return { statementAdded: true }; + } + + /** + * Grant topic publishing permissions to the given identity + */ + public grantPublish(grantee: IGrantable, topic: string): Grant { + return Grant.addToPrincipalOrResource({ + grantee, + actions: ['iot:Publish'], + resourceArns: [topicArn(this, topic)], + resource: this, + }); + } +} /** * A new topic rule */ -export class TopicRule extends Resource implements ITopicRule { +export class TopicRule extends TopicRuleBase { /** * Import topic rule attributes */ @@ -121,7 +157,7 @@ export class TopicRule extends Resource implements ITopicRule { public static fromTopicRuleAttributes(scope: Construct, id: string, attrs: TopicRuleAttributes): ITopicRule { const topicRuleArn = attrs.topicRuleArn; const ruleName = parseRuleName(attrs.topicRuleArn); - class Import extends Resource implements ITopicRule { + class Import extends TopicRuleBase { public readonly ruleName = ruleName; public readonly topicRuleArn = topicRuleArn; } @@ -139,7 +175,6 @@ export class TopicRule extends Resource implements ITopicRule { physicalName: props.ruleName, }); - if (props.errorAction) { this.addErrorAction(props.errorAction); } @@ -179,7 +214,7 @@ export class TopicRule extends Resource implements ITopicRule { public renderActions() { if (this.actions.length === 0) { - return undefined; + throw new Error('Topic invalid. Must contain at least one action'); } return this.actions; } diff --git a/packages/@aws-cdk/aws-iot/lib/util.ts b/packages/@aws-cdk/aws-iot/lib/util.ts index 2e49895a545c4..8f6f4dece8eb7 100644 --- a/packages/@aws-cdk/aws-iot/lib/util.ts +++ b/packages/@aws-cdk/aws-iot/lib/util.ts @@ -1,7 +1,42 @@ -import { Fn } from '@aws-cdk/core'; - +import { IRole, PolicyStatement, ServicePrincipal, Role } from '@aws-cdk/aws-iam'; +import { Fn, IConstruct, Stack, Arn } from '@aws-cdk/core'; export { undefinedIfAllValuesAreEmpty } from '@aws-cdk/core/lib/util'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct } from '@aws-cdk/core'; + export function parseRuleName(topicRuleArn: string): string { return Fn.select(1, Fn.split('rule/', topicRuleArn)); } +/** + * Obtain the Role for the Topic Rule + * + * If a role already exists, it will be returned. This ensures that if multiple + * events have the same target, they will share a role. + */ +export function singletonTopicRuleRole(scope: IConstruct, policyStatements: PolicyStatement[]): IRole { + const stack = Stack.of(scope); + const id = 'AllowIot'; + const existing = stack.node.tryFindChild(id) as IRole; + if (existing) { return existing; } + + const role = new Role(scope as Construct, id, { + assumedBy: new ServicePrincipal('iot.amazonaws.com'), + }); + + policyStatements.forEach(role.addToPolicy.bind(role)); + + return role; +} + +export function topicArn(scope: IConstruct, topic: string, region?: string, account?: string): string { + const stack = Stack.of(scope); + return Arn.format({ + region: region || stack.region, + account: account || stack.account, + service: 'iot', + resource: 'topic', + resourceName: topic, + }, stack); +} diff --git a/packages/@aws-cdk/aws-iot/package.json b/packages/@aws-cdk/aws-iot/package.json index e409ea5cdaf9a..b1461b23d1dcc 100644 --- a/packages/@aws-cdk/aws-iot/package.json +++ b/packages/@aws-cdk/aws-iot/package.json @@ -73,17 +73,20 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", "nodeunit-shim": "0.0.0" }, "dependencies": { "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", "constructs": "^3.2.0" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", "constructs": "^3.2.0" }, "engines": { diff --git a/packages/@aws-cdk/aws-iot/test/iot.test.ts b/packages/@aws-cdk/aws-iot/test/iot.test.ts deleted file mode 100644 index e394ef336bfb4..0000000000000 --- a/packages/@aws-cdk/aws-iot/test/iot.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assert/jest'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts index d3942a01662ce..da085a478faf5 100644 --- a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts +++ b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts @@ -1,5 +1,6 @@ import { expect, haveResource } from '@aws-cdk/assert'; -import { Stack } from '@aws-cdk/core'; +import { Role, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { App, Stack } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; import { TopicRule, ITopicRuleAction, TopicRuleActionConfig } from '../lib'; @@ -127,6 +128,68 @@ nodeunitShim({ })); test.done(); }, + 'throws when not given actions'(test: Test) { + // GIVEN + const app = new App(); + const stack = new Stack(app); + // WHEN + new TopicRule(stack, 'IotTopicRule', { + sql: 'SELECT * FROM \'topic/subtopic\'', + }); + test.throws(() => app.synth(), /Topic invalid/); + test.done(); + }, + 'grant publish to topic arn'(test: Test) { + const stack = new Stack(); + const role = new Role(stack, 'MyRole', { + assumedBy: new ServicePrincipal('iot.amazonaws.com'), + }); + const topic = new TopicRule(stack, 'IotTopicRule', { + sql: 'SELECT * FROM \'topic/subtopic\'', + actions: [new DummyAction()], + }); + topic.grantPublish(role, 'coffee/shops'); + expect(stack).to(haveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { Service: 'iot.amazonaws.com' }, + }, + ], + Version: '2012-10-17', + }, + })); + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'iot:Publish', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iot:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':topic/coffee/shops', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyRoleDefaultPolicyA36BE1DD', + Roles: [{ Ref: 'MyRoleF48FFE04' }], + })); + test.done(); + }, }); class DummyAction implements ITopicRuleAction {