diff --git a/packages/@aws-cdk/aws-iot-actions/README.md b/packages/@aws-cdk/aws-iot-actions/README.md index 2572003a0fab7..efc5240ffb3af 100644 --- a/packages/@aws-cdk/aws-iot-actions/README.md +++ b/packages/@aws-cdk/aws-iot-actions/README.md @@ -32,6 +32,7 @@ Currently supported are: - Send messages to SQS queues - Publish messages on SNS topics - Write messages into columns of DynamoDB +- Put messages IoT Events input ## Republish a message to another MQTT topic @@ -73,7 +74,7 @@ new iot.TopicRule(this, 'TopicRule', { ## Put objects to a S3 bucket -The code snippet below creates an AWS IoT Rule that put objects to a S3 bucket +The code snippet below creates an AWS IoT Rule that puts objects to a S3 bucket when it is triggered. ```ts @@ -126,7 +127,7 @@ new iot.TopicRule(this, 'TopicRule', { ## Put logs to CloudWatch Logs -The code snippet below creates an AWS IoT Rule that put logs to CloudWatch Logs +The code snippet below creates an AWS IoT Rule that puts logs to CloudWatch Logs when it is triggered. ```ts @@ -194,7 +195,7 @@ const topicRule = new iot.TopicRule(this, 'TopicRule', { ## Put records to Kinesis Data stream -The code snippet below creates an AWS IoT Rule that put records to Kinesis Data +The code snippet below creates an AWS IoT Rule that puts records to Kinesis Data stream when it is triggered. ```ts @@ -214,7 +215,7 @@ const topicRule = new iot.TopicRule(this, 'TopicRule', { ## Put records to Kinesis Data Firehose stream -The code snippet below creates an AWS IoT Rule that put records to Put records +The code snippet below creates an AWS IoT Rule that puts records to Put records to Kinesis Data Firehose stream when it is triggered. ```ts @@ -299,3 +300,31 @@ const topicRule = new iot.TopicRule(this, 'TopicRule', { ], }); ``` + +## Put messages IoT Events input + +The code snippet below creates an AWS IoT Rule that puts messages +to an IoT Events input when it is triggered: + +```ts +import * as iotevents from '@aws-cdk/aws-iotevents'; +import * as iam from '@aws-cdk/aws-iam'; + +declare const role: iam.IRole; + +const input = new iotevents.Input(this, 'MyInput', { + attributeJsonPaths: ['payload.temperature', 'payload.transactionId'], +}); +const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323( + "SELECT * FROM 'device/+/data'", + ), + actions: [ + new actions.IotEventsPutMessageAction(input, { + batchMode: true, // optional property, default is 'false' + messageId: '${payload.transactionId}', // optional property, default is a new UUID + role: role, // optional property, default is a new UUID + }), + ], +}); +``` diff --git a/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-logs-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-logs-action.ts index fb8f2779f32e7..712118f1c81de 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-logs-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-logs-action.ts @@ -27,7 +27,10 @@ export class CloudWatchLogsAction implements iot.IAction { this.role = props.role; } - bind(rule: iot.ITopicRule): iot.ActionConfig { + /** + * @internal + */ + public _bind(rule: iot.ITopicRule): iot.ActionConfig { const role = this.role ?? singletonActionRole(rule); this.logGroup.grantWrite(role); this.logGroup.grant(role, 'logs:DescribeLogStreams'); diff --git a/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-put-metric-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-put-metric-action.ts index 90d7658e4a493..c695808794e16 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-put-metric-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-put-metric-action.ts @@ -57,7 +57,10 @@ export class CloudWatchPutMetricAction implements iot.IAction { constructor(private readonly props: CloudWatchPutMetricActionProps) { } - bind(rule: iot.ITopicRule): iot.ActionConfig { + /** + * @internal + */ + public _bind(rule: iot.ITopicRule): iot.ActionConfig { const role = this.props.role ?? singletonActionRole(rule); cloudwatch.Metric.grantPutMetricData(role); diff --git a/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-set-alarm-state-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-set-alarm-state-action.ts index 07e42c04a2204..5b702a86f3342 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-set-alarm-state-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/cloudwatch-set-alarm-state-action.ts @@ -31,7 +31,10 @@ export class CloudWatchSetAlarmStateAction implements iot.IAction { ) { } - bind(topicRule: iot.ITopicRule): iot.ActionConfig { + /** + * @internal + */ + public _bind(topicRule: iot.ITopicRule): iot.ActionConfig { const role = this.props.role ?? singletonActionRole(topicRule); role.addToPrincipalPolicy(new iam.PolicyStatement({ actions: ['cloudwatch:SetAlarmState'], diff --git a/packages/@aws-cdk/aws-iot-actions/lib/dynamodbv2-put-item-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/dynamodbv2-put-item-action.ts index 606ad3f0036f0..0d52ed27a179c 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/dynamodbv2-put-item-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/dynamodbv2-put-item-action.ts @@ -24,7 +24,10 @@ export class DynamoDBv2PutItemAction implements iot.IAction { this.role = props.role; } - bind(rule: iot.ITopicRule): iot.ActionConfig { + /** + * @internal + */ + public _bind(rule: iot.ITopicRule): iot.ActionConfig { const role = this.role ?? singletonActionRole(rule); role.addToPrincipalPolicy(new iam.PolicyStatement({ actions: ['dynamodb:PutItem'], diff --git a/packages/@aws-cdk/aws-iot-actions/lib/firehose-put-record-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/firehose-put-record-action.ts index e9583ce1c87e3..0112c1c166000 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/firehose-put-record-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/firehose-put-record-action.ts @@ -70,7 +70,10 @@ export class FirehosePutRecordAction implements iot.IAction { this.role = props.role; } - bind(rule: iot.ITopicRule): iot.ActionConfig { + /** + * @internal + */ + public _bind(rule: iot.ITopicRule): iot.ActionConfig { const role = this.role ?? singletonActionRole(rule); this.stream.grantPutRecords(role); diff --git a/packages/@aws-cdk/aws-iot-actions/lib/index.ts b/packages/@aws-cdk/aws-iot-actions/lib/index.ts index 91dd24ac32cd1..0a9d23398f59b 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/index.ts @@ -4,6 +4,7 @@ export * from './cloudwatch-set-alarm-state-action'; export * from './common-action-props'; export * from './dynamodbv2-put-item-action'; export * from './firehose-put-record-action'; +export * from './iotevents-put-message-action'; export * from './iot-republish-action'; export * from './kinesis-put-record-action'; export * from './lambda-function-action'; diff --git a/packages/@aws-cdk/aws-iot-actions/lib/iot-republish-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/iot-republish-action.ts index 77aadb876c4d9..eb3aa485cdba5 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/iot-republish-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/iot-republish-action.ts @@ -52,7 +52,10 @@ export class IotRepublishMqttAction implements iot.IAction { this.role = props.role; } - bind(rule: iot.ITopicRule): iot.ActionConfig { + /** + * @internal + */ + public _bind(rule: iot.ITopicRule): iot.ActionConfig { const role = this.role ?? singletonActionRole(rule); role.addToPrincipalPolicy(new iam.PolicyStatement({ actions: ['iot:Publish'], diff --git a/packages/@aws-cdk/aws-iot-actions/lib/iotevents-put-message-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/iotevents-put-message-action.ts new file mode 100644 index 0000000000000..9964bedc3ab05 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/lib/iotevents-put-message-action.ts @@ -0,0 +1,75 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as iotevents from '@aws-cdk/aws-iotevents'; +import { CommonActionProps } from './common-action-props'; +import { singletonActionRole } from './private/role'; + +/** + * Configuration properties of an action for the IoT Events. + */ +export interface IotEventsPutMessageActionProps extends CommonActionProps { + /** + * Whether to process the event actions as a batch. + * + * When batchMode is true, you can't specify a messageId. + * + * When batchMode is true and the rule SQL statement evaluates to an Array, + * each Array element is treated as a separate message when Events by calling BatchPutMessage. + * The resulting array can't have more than 10 messages. + * + * @default false + */ + readonly batchMode?: boolean; + + /** + * The ID of the message. + * + * When batchMode is true, you can't specify a messageId--a new UUID value will be assigned. + * Assign a value to this property to ensure that only one input (message) with a given messageId will be processed by an AWS IoT Events detector. + * + * @default - none -- a new UUID value will be assigned + */ + readonly messageId?: string; +} + +/** + * The action to put the message from an MQTT message to the IoT Events input. + */ +export class IotEventsPutMessageAction implements iot.IAction { + private readonly batchMode?: boolean; + private readonly messageId?: string; + private readonly role?: iam.IRole; + + /** + * @param input The IoT Events input to put messages. + * @param props Optional properties to not use default + */ + constructor(private readonly input: iotevents.IInput, props: IotEventsPutMessageActionProps = {}) { + this.batchMode = props.batchMode; + this.messageId = props.messageId; + this.role = props.role; + + if (this.batchMode && this.messageId) { + throw new Error('messageId is not allowed when batchMode is true'); + } + } + + /** + * @internal + */ + public _bind(rule: iot.ITopicRule): iot.ActionConfig { + const role = this.role ?? singletonActionRole(rule); + this.input.grantWrite(role); + + return { + configuration: { + iotEvents: { + batchMode: this.batchMode, + inputName: this.input.inputName, + messageId: this.messageId, + roleArn: role.roleArn, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot-actions/lib/kinesis-put-record-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/kinesis-put-record-action.ts index 6baa5976bccf4..b2b156c831706 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/kinesis-put-record-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/kinesis-put-record-action.ts @@ -38,7 +38,10 @@ export class KinesisPutRecordAction implements iot.IAction { this.role = props.role; } - bind(rule: iot.ITopicRule): iot.ActionConfig { + /** + * @internal + */ + public _bind(rule: iot.ITopicRule): iot.ActionConfig { const role = this.role ?? singletonActionRole(rule); role.addToPrincipalPolicy(new iam.PolicyStatement({ actions: ['kinesis:PutRecord'], diff --git a/packages/@aws-cdk/aws-iot-actions/lib/lambda-function-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/lambda-function-action.ts index 60cf056d6e5ba..82c8cd1d93ab1 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/lambda-function-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/lambda-function-action.ts @@ -12,7 +12,10 @@ export class LambdaFunctionAction implements iot.IAction { */ constructor(private readonly func: lambda.IFunction) {} - bind(topicRule: iot.ITopicRule): iot.ActionConfig { + /** + * @internal + */ + public _bind(topicRule: iot.ITopicRule): iot.ActionConfig { this.func.addPermission(`${Names.nodeUniqueId(topicRule.node)}:IotLambdaFunctionAction`, { action: 'lambda:InvokeFunction', principal: new iam.ServicePrincipal('iot.amazonaws.com'), diff --git a/packages/@aws-cdk/aws-iot-actions/lib/s3-put-object-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/s3-put-object-action.ts index f690bf813a922..36fb19d1b0993 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/s3-put-object-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/s3-put-object-action.ts @@ -46,7 +46,10 @@ export class S3PutObjectAction implements iot.IAction { this.role = props.role; } - bind(rule: iot.ITopicRule): iot.ActionConfig { + /** + * @internal + */ + public _bind(rule: iot.ITopicRule): iot.ActionConfig { const role = this.role ?? singletonActionRole(rule); role.addToPrincipalPolicy(new iam.PolicyStatement({ actions: ['s3:PutObject'], diff --git a/packages/@aws-cdk/aws-iot-actions/lib/sns-topic-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/sns-topic-action.ts index 701eeb0d8b39e..2c6b21157945b 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/sns-topic-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/sns-topic-action.ts @@ -58,7 +58,10 @@ export class SnsTopicAction implements iot.IAction { this.messageFormat = props.messageFormat; } - bind(rule: iot.ITopicRule): iot.ActionConfig { + /** + * @internal + */ + public _bind(rule: iot.ITopicRule): iot.ActionConfig { const role = this.role ?? singletonActionRole(rule); this.topic.grantPublish(role); @@ -72,4 +75,4 @@ export class SnsTopicAction implements iot.IAction { }, }; } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-iot-actions/lib/sqs-queue-action.ts b/packages/@aws-cdk/aws-iot-actions/lib/sqs-queue-action.ts index 224d5190d5477..86de4dbe41109 100644 --- a/packages/@aws-cdk/aws-iot-actions/lib/sqs-queue-action.ts +++ b/packages/@aws-cdk/aws-iot-actions/lib/sqs-queue-action.ts @@ -34,7 +34,10 @@ export class SqsQueueAction implements iot.IAction { this.useBase64 = props.useBase64; } - bind(rule: iot.ITopicRule): iot.ActionConfig { + /** + * @internal + */ + public _bind(rule: iot.ITopicRule): iot.ActionConfig { const role = this.role ?? singletonActionRole(rule); role.addToPrincipalPolicy(new iam.PolicyStatement({ actions: ['sqs:SendMessage'], diff --git a/packages/@aws-cdk/aws-iot-actions/package.json b/packages/@aws-cdk/aws-iot-actions/package.json index 44b052ec6db66..bf4691581742c 100644 --- a/packages/@aws-cdk/aws-iot-actions/package.json +++ b/packages/@aws-cdk/aws-iot-actions/package.json @@ -92,6 +92,7 @@ "@aws-cdk/aws-dynamodb": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-iotevents": "0.0.0", "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-kinesisfirehose": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", @@ -109,6 +110,7 @@ "@aws-cdk/aws-dynamodb": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-iot": "0.0.0", + "@aws-cdk/aws-iotevents": "0.0.0", "@aws-cdk/aws-kinesis": "0.0.0", "@aws-cdk/aws-kinesisfirehose": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", diff --git a/packages/@aws-cdk/aws-iot-actions/test/iot/integ.iotevents-put-message-action.ts b/packages/@aws-cdk/aws-iot-actions/test/iot/integ.iotevents-put-message-action.ts new file mode 100644 index 0000000000000..17b0f0c8058e8 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/iot/integ.iotevents-put-message-action.ts @@ -0,0 +1,52 @@ +import * as iot from '@aws-cdk/aws-iot'; +import * as iotevents from '@aws-cdk/aws-iotevents'; +import * as logs from '@aws-cdk/aws-logs'; +import * as cdk from '@aws-cdk/core'; +import { IntegTest } from '@aws-cdk/integ-tests'; +import * as actions from '../../lib'; + +class TestStack extends cdk.Stack { + public readonly detectorModelName: string; + + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const logGroup = new logs.LogGroup(this, 'logs', { removalPolicy: cdk.RemovalPolicy.DESTROY }); + const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323( + "SELECT * FROM 'device/+/data'", + ), + errorAction: new actions.CloudWatchLogsAction(logGroup), + }); + + const input = new iotevents.Input(this, 'MyInput', { + attributeJsonPaths: ['payload.deviceId'], + }); + + const detectorModel = new iotevents.DetectorModel(this, 'MyDetectorModel', { + detectorKey: 'payload.deviceId', + initialState: new iotevents.State({ + stateName: 'initialState', + onEnter: [{ + eventName: 'enter', + condition: iotevents.Expression.currentInput(input), + }], + }), + }); + + topicRule.addAction( + new actions.IotEventsPutMessageAction(input, { + batchMode: true, + }), + ); + + this.detectorModelName = detectorModel.detectorModelName; + } +} + +// App +const app = new cdk.App(); +const stack = new TestStack(app, 'iotevents-put-message-action-test-stack'); +new IntegTest(app, 'iotevents', { testCases: [stack] }); + +app.synth(); diff --git a/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..588d7b269d34f --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"20.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/integ.json b/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/integ.json new file mode 100644 index 0000000000000..a9baad7907b18 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/integ.json @@ -0,0 +1,11 @@ +{ + "version": "20.0.0", + "testCases": { + "iotevents/DefaultTest": { + "stacks": [ + "iotevents-put-message-action-test-stack" + ], + "assertionStack": "ioteventsDefaultTestDeployAssertE216288D" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/iotevents-put-message-action-test-stack.template.json b/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/iotevents-put-message-action-test-stack.template.json new file mode 100644 index 0000000000000..0cc54b5ce90d9 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/iotevents-put-message-action-test-stack.template.json @@ -0,0 +1,192 @@ +{ + "Resources": { + "logs0B6081B1": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "TopicRuleTopicRuleActionRole246C4F77": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:DescribeLogStreams", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "logs0B6081B1", + "Arn" + ] + } + }, + { + "Action": "iotevents:BatchPutMessage", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iotevents:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":input/", + { + "Ref": "MyInput08947B23" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + }, + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "IotEvents": { + "BatchMode": true, + "InputName": { + "Ref": "MyInput08947B23" + }, + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + } + } + } + ], + "AwsIotSqlVersion": "2016-03-23", + "ErrorAction": { + "CloudwatchLogs": { + "LogGroupName": { + "Ref": "logs0B6081B1" + }, + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + } + } + }, + "Sql": "SELECT * FROM 'device/+/data'" + } + } + }, + "MyInput08947B23": { + "Type": "AWS::IoTEvents::Input", + "Properties": { + "InputDefinition": { + "Attributes": [ + { + "JsonPath": "payload.deviceId" + } + ] + } + } + }, + "MyDetectorModelDetectorModelRoleF2FB4D88": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iotevents.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyDetectorModel559C0B0E": { + "Type": "AWS::IoTEvents::DetectorModel", + "Properties": { + "DetectorModelDefinition": { + "InitialStateName": "initialState", + "States": [ + { + "OnEnter": { + "Events": [ + { + "Condition": { + "Fn::Join": [ + "", + [ + "currentInput(\"", + { + "Ref": "MyInput08947B23" + }, + "\")" + ] + ] + }, + "EventName": "enter" + } + ] + }, + "StateName": "initialState" + } + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "MyDetectorModelDetectorModelRoleF2FB4D88", + "Arn" + ] + }, + "Key": "payload.deviceId" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/ioteventsDefaultTestDeployAssertE216288D.template.json b/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/ioteventsDefaultTestDeployAssertE216288D.template.json new file mode 100644 index 0000000000000..9e26dfeeb6e64 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/ioteventsDefaultTestDeployAssertE216288D.template.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..17de49a792718 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/manifest.json @@ -0,0 +1,73 @@ +{ + "version": "20.0.0", + "artifacts": { + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "iotevents-put-message-action-test-stack": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "iotevents-put-message-action-test-stack.template.json", + "validateOnSynth": false + }, + "metadata": { + "/iotevents-put-message-action-test-stack/logs/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "logs0B6081B1" + } + ], + "/iotevents-put-message-action-test-stack/TopicRule/TopicRuleActionRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TopicRuleTopicRuleActionRole246C4F77" + } + ], + "/iotevents-put-message-action-test-stack/TopicRule/TopicRuleActionRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687" + } + ], + "/iotevents-put-message-action-test-stack/TopicRule/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TopicRule40A4EA44" + } + ], + "/iotevents-put-message-action-test-stack/MyInput/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyInput08947B23" + } + ], + "/iotevents-put-message-action-test-stack/MyDetectorModel/DetectorModelRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyDetectorModelDetectorModelRoleF2FB4D88" + } + ], + "/iotevents-put-message-action-test-stack/MyDetectorModel/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyDetectorModel559C0B0E" + } + ] + }, + "displayName": "iotevents-put-message-action-test-stack" + }, + "ioteventsDefaultTestDeployAssertE216288D": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "ioteventsDefaultTestDeployAssertE216288D.template.json", + "validateOnSynth": false + }, + "displayName": "iotevents/DefaultTest/DeployAssert" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/tree.json b/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/tree.json new file mode 100644 index 0000000000000..35a2f68549cb8 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.integ.snapshot/tree.json @@ -0,0 +1,379 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.58" + } + }, + "iotevents-put-message-action-test-stack": { + "id": "iotevents-put-message-action-test-stack", + "path": "iotevents-put-message-action-test-stack", + "children": { + "logs": { + "id": "logs", + "path": "iotevents-put-message-action-test-stack/logs", + "children": { + "Resource": { + "id": "Resource", + "path": "iotevents-put-message-action-test-stack/logs/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Logs::LogGroup", + "aws:cdk:cloudformation:props": { + "retentionInDays": 731 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-logs.CfnLogGroup", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-logs.LogGroup", + "version": "0.0.0" + } + }, + "TopicRule": { + "id": "TopicRule", + "path": "iotevents-put-message-action-test-stack/TopicRule", + "children": { + "TopicRuleActionRole": { + "id": "TopicRuleActionRole", + "path": "iotevents-put-message-action-test-stack/TopicRule/TopicRuleActionRole", + "children": { + "Resource": { + "id": "Resource", + "path": "iotevents-put-message-action-test-stack/TopicRule/TopicRuleActionRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iot.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "iotevents-put-message-action-test-stack/TopicRule/TopicRuleActionRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "iotevents-put-message-action-test-stack/TopicRule/TopicRuleActionRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogStream", + "logs:DescribeLogStreams", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "logs0B6081B1", + "Arn" + ] + } + }, + { + "Action": "iotevents:BatchPutMessage", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iotevents:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":input/", + { + "Ref": "MyInput08947B23" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "iotevents-put-message-action-test-stack/TopicRule/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IoT::TopicRule", + "aws:cdk:cloudformation:props": { + "topicRulePayload": { + "actions": [ + { + "iotEvents": { + "batchMode": true, + "inputName": { + "Ref": "MyInput08947B23" + }, + "roleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + } + } + } + ], + "awsIotSqlVersion": "2016-03-23", + "errorAction": { + "cloudwatchLogs": { + "logGroupName": { + "Ref": "logs0B6081B1" + }, + "roleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + } + } + }, + "sql": "SELECT * FROM 'device/+/data'" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iot.CfnTopicRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iot.TopicRule", + "version": "0.0.0" + } + }, + "MyInput": { + "id": "MyInput", + "path": "iotevents-put-message-action-test-stack/MyInput", + "children": { + "Resource": { + "id": "Resource", + "path": "iotevents-put-message-action-test-stack/MyInput/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IoTEvents::Input", + "aws:cdk:cloudformation:props": { + "inputDefinition": { + "attributes": [ + { + "jsonPath": "payload.deviceId" + } + ] + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iotevents.CfnInput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iotevents.Input", + "version": "0.0.0" + } + }, + "MyDetectorModel": { + "id": "MyDetectorModel", + "path": "iotevents-put-message-action-test-stack/MyDetectorModel", + "children": { + "DetectorModelRole": { + "id": "DetectorModelRole", + "path": "iotevents-put-message-action-test-stack/MyDetectorModel/DetectorModelRole", + "children": { + "Resource": { + "id": "Resource", + "path": "iotevents-put-message-action-test-stack/MyDetectorModel/DetectorModelRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iotevents.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "iotevents-put-message-action-test-stack/MyDetectorModel/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IoTEvents::DetectorModel", + "aws:cdk:cloudformation:props": { + "detectorModelDefinition": { + "initialStateName": "initialState", + "states": [ + { + "stateName": "initialState", + "onEnter": { + "events": [ + { + "eventName": "enter", + "condition": { + "Fn::Join": [ + "", + [ + "currentInput(\"", + { + "Ref": "MyInput08947B23" + }, + "\")" + ] + ] + } + } + ] + } + } + ] + }, + "roleArn": { + "Fn::GetAtt": [ + "MyDetectorModelDetectorModelRoleF2FB4D88", + "Arn" + ] + }, + "key": "payload.deviceId" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iotevents.CfnDetectorModel", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iotevents.DetectorModel", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "iotevents": { + "id": "iotevents", + "path": "iotevents", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "iotevents/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "iotevents/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.58" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "iotevents/DefaultTest/DeployAssert", + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.test.ts b/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.test.ts new file mode 100644 index 0000000000000..9190207522ac4 --- /dev/null +++ b/packages/@aws-cdk/aws-iot-actions/test/iot/iotevents-put-message-action.test.ts @@ -0,0 +1,145 @@ +import { Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; +import * as iot from '@aws-cdk/aws-iot'; +import * as iotevents from '@aws-cdk/aws-iotevents'; +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +let stack: cdk.Stack; +let topicRule: iot.TopicRule; +let input: iotevents.IInput; + +beforeEach(() => { + stack = new cdk.Stack(); + topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + }); + input = iotevents.Input.fromInputName(stack, 'MyInput', 'my_input'); +}); + + +test('Default IoT Events input action', () => { + // WHEN + topicRule.addAction( + new actions.IotEventsPutMessageAction(input), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + IotEvents: { + InputName: 'my_input', + RoleArn: { + 'Fn::GetAtt': ['MyTopicRuleTopicRuleActionRoleCE2D05DA', 'Arn'], + }, + }, + }, + ], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'iot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'iotevents:BatchPutMessage', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':iotevents:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':input/my_input', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [ + { Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }, + ], + }); +}); + +test('can set batchMode', () => { + // WHEN + topicRule.addAction( + new actions.IotEventsPutMessageAction(input, { batchMode: true }), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [{ IotEvents: { BatchMode: true } }], + }, + }); +}); + +test('can set messageId', () => { + // WHEN + topicRule.addAction( + new actions.IotEventsPutMessageAction(input, { messageId: '${topic()}-${timestamp()}' }), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [{ IotEvents: { MessageId: '${topic()}-${timestamp()}' } }], + }, + }); +}); + +test('can set role', () => { + const role = iam.Role.fromRoleArn(stack, 'MyRole', 'arn:aws:iam::123456789012:role/ForTest'); + + // WHEN + topicRule.addAction( + new actions.IotEventsPutMessageAction(input, { role }), + ); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [{ IotEvents: { RoleArn: 'arn:aws:iam::123456789012:role/ForTest' } }], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyName: 'MyRolePolicy64AB00A5', + Roles: ['ForTest'], + }); +}); + +test('cannot set both batchMode and messageId', () => { + expect(() => { + new actions.IotEventsPutMessageAction(input, { + batchMode: true, + messageId: '${topic()}-${timestamp()}', + }); + }).toThrow('messageId is not allowed when batchMode is true'); +}); diff --git a/packages/@aws-cdk/aws-iot/lib/action.ts b/packages/@aws-cdk/aws-iot/lib/action.ts index f22daf6194b1c..7e5ce15cae132 100644 --- a/packages/@aws-cdk/aws-iot/lib/action.ts +++ b/packages/@aws-cdk/aws-iot/lib/action.ts @@ -9,8 +9,9 @@ export interface IAction { * Returns the topic rule action specification. * * @param topicRule The TopicRule that would trigger this action. + * @internal */ - bind(topicRule: ITopicRule): ActionConfig; + _bind(topicRule: ITopicRule): ActionConfig; } /** diff --git a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts index 89f40c3797d97..0a9f8a3891381 100644 --- a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts +++ b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts @@ -124,7 +124,7 @@ export class TopicRule extends Resource implements ITopicRule { actions: Lazy.any({ produce: () => this.actions }), awsIotSqlVersion: sqlConfig.awsIotSqlVersion, description: props.description, - errorAction: props.errorAction?.bind(this).configuration, + errorAction: props.errorAction?._bind(this).configuration, ruleDisabled: props.enabled === undefined ? undefined : !props.enabled, sql: sqlConfig.sql, }, @@ -148,7 +148,7 @@ export class TopicRule extends Resource implements ITopicRule { * @param action the action to associate with the topic rule. */ public addAction(action: IAction): void { - const { configuration } = action.bind(this); + const { configuration } = action._bind(this); const keys = Object.keys(configuration); if (keys.length === 0) { diff --git a/packages/@aws-cdk/aws-iot/package.json b/packages/@aws-cdk/aws-iot/package.json index e846a30a6ce5e..985ea75c3e20d 100644 --- a/packages/@aws-cdk/aws-iot/package.json +++ b/packages/@aws-cdk/aws-iot/package.json @@ -100,6 +100,11 @@ "engines": { "node": ">= 14.15.0" }, + "awslint": { + "exclude": [ + "no-unused-type:@aws-cdk/aws-iot.ActionConfig" + ] + }, "stability": "experimental", "maturity": "experimental", "awscdkio": { diff --git a/packages/@aws-cdk/aws-iot/test/integ.topic-rule.ts b/packages/@aws-cdk/aws-iot/test/integ.topic-rule.ts index 0f4bab54a9d2a..c5a602141bda6 100644 --- a/packages/@aws-cdk/aws-iot/test/integ.topic-rule.ts +++ b/packages/@aws-cdk/aws-iot/test/integ.topic-rule.ts @@ -12,7 +12,7 @@ class TestStack extends cdk.Stack { sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id FROM 'device/+/data'"), actions: [ { - bind: () => ({ + _bind: () => ({ configuration: { http: { url: 'https://example.com' }, }, 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 5f84201a37e5d..aa79c769c2453 100644 --- a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts +++ b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts @@ -134,14 +134,14 @@ test('can set actions', () => { const stack = new cdk.Stack(); const action1: iot.IAction = { - bind: () => ({ + _bind: () => ({ configuration: { http: { url: 'http://example.com' }, }, }), }; const action2: iot.IAction = { - bind: () => ({ + _bind: () => ({ configuration: { lambda: { functionArn: 'test-functionArn' }, }, @@ -171,14 +171,14 @@ test('can add an action', () => { sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"), }); topicRule.addAction({ - bind: () => ({ + _bind: () => ({ configuration: { http: { url: 'http://example.com' }, }, }), }); topicRule.addAction({ - bind: () => ({ + _bind: () => ({ configuration: { lambda: { functionArn: 'test-functionArn' }, }, @@ -203,7 +203,7 @@ test('cannot add an action as empty object', () => { }); const emptyKeysAction: iot.IAction = { - bind: () => ({ + _bind: () => ({ configuration: {}, }), }; @@ -220,7 +220,7 @@ test('cannot add an action that have multiple keys', () => { }); const multipleKeysAction: iot.IAction = { - bind: () => ({ + _bind: () => ({ configuration: { http: { url: 'http://example.com' }, lambda: { functionArn: 'test-functionArn' }, @@ -237,7 +237,7 @@ test('can set errorAction', () => { const stack = new cdk.Stack(); const action: iot.IAction = { - bind: () => ({ + _bind: () => ({ configuration: { http: { url: 'http://example.com' }, }, diff --git a/tools/@aws-cdk/pkglint/lib/rules.ts b/tools/@aws-cdk/pkglint/lib/rules.ts index ffd1ba1e4eb22..167afc9a24b78 100644 --- a/tools/@aws-cdk/pkglint/lib/rules.ts +++ b/tools/@aws-cdk/pkglint/lib/rules.ts @@ -1715,7 +1715,7 @@ export class NoExperimentalDependents extends ValidationRule { ['@aws-cdk/aws-apigatewayv2-authorizers', ['@aws-cdk/aws-apigatewayv2']], ['@aws-cdk/aws-events-targets', ['@aws-cdk/aws-kinesisfirehose']], ['@aws-cdk/aws-kinesisfirehose-destinations', ['@aws-cdk/aws-kinesisfirehose']], - ['@aws-cdk/aws-iot-actions', ['@aws-cdk/aws-iot', '@aws-cdk/aws-kinesisfirehose']], + ['@aws-cdk/aws-iot-actions', ['@aws-cdk/aws-iot', '@aws-cdk/aws-kinesisfirehose', '@aws-cdk/aws-iotevents']], ['@aws-cdk/aws-iotevents-actions', ['@aws-cdk/aws-iotevents']], ]);