From 924ab832e76d5c9bd583e3e7839a4e3502d1b812 Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Mon, 4 Oct 2021 20:02:11 +0900 Subject: [PATCH] feat(iot): implements some actions --- package.json | 2 + packages/@aws-cdk/aws-iot/NOTICE | 30 ++ packages/@aws-cdk/aws-iot/lib/action.ts | 4 +- .../lib/actions/cloudwatch-alarm-action.ts | 73 +++++ .../lib/actions/cloudwatch-logs-action.ts | 47 +++ .../lib/actions/cloudwatch-metric-action.ts | 84 ++++++ .../aws-iot/lib/actions/dynamodb-action.ts | 92 ++++++ .../aws-iot/lib/actions/dynamodb-v2-action.ts | 46 +++ .../@aws-cdk/aws-iot/lib/actions/index.ts | 9 + .../aws-iot/lib/actions/lambda-action.ts | 31 ++ .../@aws-cdk/aws-iot/lib/actions/s3-action.ts | 72 +++++ .../aws-iot/lib/actions/sns-action.ts | 58 ++++ .../aws-iot/lib/actions/sqs-action.ts | 54 ++++ packages/@aws-cdk/aws-iot/lib/index.ts | 1 + .../@aws-cdk/aws-iot/lib/topic-rule-ref.ts | 21 ++ packages/@aws-cdk/aws-iot/lib/topic-rule.ts | 26 +- packages/@aws-cdk/aws-iot/package.json | 18 ++ .../cloudwatch-alarm-action.test.ts | 256 ++++++++++++++++ ...nteg.cloudwatch-alarm-action.expected.json | 86 ++++++ .../integ.cloudwatch-alarm-action.ts | 35 +++ .../cloudwatch-logs-action.test.ts | 172 +++++++++++ ...integ.cloudwatch-logs-action.expected.json | 84 ++++++ .../integ.cloudwatch-logs-action.ts | 27 ++ .../cloudwatch-metric-action.test.ts | 226 +++++++++++++++ ...teg.cloudwatch-metric-action.expected.json | 69 +++++ .../integ.cloudwatch-metric-action.ts | 29 ++ .../dynamodb-v2/dynamodb-v2-action.test.ts | 192 ++++++++++++ .../integ.dynamodb-v2-action.expected.json | 110 +++++++ .../dynamodb-v2/integ.dynamodb-v2-action.ts | 29 ++ .../actions/dynamodb/dynamodb-action.test.ts | 274 ++++++++++++++++++ .../integ.dynamodb-action.expected.json | 115 ++++++++ .../actions/dynamodb/integ.dynamodb-action.ts | 34 +++ .../lambda/integ.lambda-action.expected.json | 98 +++++++ .../actions/lambda/integ.lambda-action.ts | 33 +++ .../test/actions/lambda/lambda-action.test.ts | 73 +++++ .../actions/s3/integ.s3-action.expected.json | 121 ++++++++ .../test/actions/s3/integ.s3-action.ts | 29 ++ .../aws-iot/test/actions/s3/s3-action.test.ts | 257 ++++++++++++++++ .../sns/integ.sns-action.expected.json | 108 +++++++ .../test/actions/sns/integ.sns-action.ts | 27 ++ .../test/actions/sns/sns-action.test.ts | 195 +++++++++++++ .../sqs/integ.sqs-action.expected.json | 113 ++++++++ .../test/actions/sqs/integ.sqs-action.ts | 29 ++ .../test/actions/sqs/sqs-action.test.ts | 205 +++++++++++++ .../@aws-cdk/aws-iot/test/topic-rule.test.ts | 23 ++ 45 files changed, 3713 insertions(+), 4 deletions(-) create mode 100644 packages/@aws-cdk/aws-iot/lib/actions/cloudwatch-alarm-action.ts create mode 100644 packages/@aws-cdk/aws-iot/lib/actions/cloudwatch-logs-action.ts create mode 100644 packages/@aws-cdk/aws-iot/lib/actions/cloudwatch-metric-action.ts create mode 100644 packages/@aws-cdk/aws-iot/lib/actions/dynamodb-action.ts create mode 100644 packages/@aws-cdk/aws-iot/lib/actions/dynamodb-v2-action.ts create mode 100644 packages/@aws-cdk/aws-iot/lib/actions/lambda-action.ts create mode 100644 packages/@aws-cdk/aws-iot/lib/actions/s3-action.ts create mode 100644 packages/@aws-cdk/aws-iot/lib/actions/sns-action.ts create mode 100644 packages/@aws-cdk/aws-iot/lib/actions/sqs-action.ts create mode 100644 packages/@aws-cdk/aws-iot/lib/topic-rule-ref.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/cloudwatch-alarm/cloudwatch-alarm-action.test.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/cloudwatch-alarm/integ.cloudwatch-alarm-action.expected.json create mode 100644 packages/@aws-cdk/aws-iot/test/actions/cloudwatch-alarm/integ.cloudwatch-alarm-action.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/cloudwatch-logs/cloudwatch-logs-action.test.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/cloudwatch-logs/integ.cloudwatch-logs-action.expected.json create mode 100644 packages/@aws-cdk/aws-iot/test/actions/cloudwatch-logs/integ.cloudwatch-logs-action.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/cloudwatch-metric/cloudwatch-metric-action.test.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/cloudwatch-metric/integ.cloudwatch-metric-action.expected.json create mode 100644 packages/@aws-cdk/aws-iot/test/actions/cloudwatch-metric/integ.cloudwatch-metric-action.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/dynamodb-v2/dynamodb-v2-action.test.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/dynamodb-v2/integ.dynamodb-v2-action.expected.json create mode 100644 packages/@aws-cdk/aws-iot/test/actions/dynamodb-v2/integ.dynamodb-v2-action.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/dynamodb/dynamodb-action.test.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/dynamodb/integ.dynamodb-action.expected.json create mode 100644 packages/@aws-cdk/aws-iot/test/actions/dynamodb/integ.dynamodb-action.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/lambda/integ.lambda-action.expected.json create mode 100644 packages/@aws-cdk/aws-iot/test/actions/lambda/integ.lambda-action.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/lambda/lambda-action.test.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/s3/integ.s3-action.expected.json create mode 100644 packages/@aws-cdk/aws-iot/test/actions/s3/integ.s3-action.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/s3/s3-action.test.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/sns/integ.sns-action.expected.json create mode 100644 packages/@aws-cdk/aws-iot/test/actions/sns/integ.sns-action.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/sns/sns-action.test.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/sqs/integ.sqs-action.expected.json create mode 100644 packages/@aws-cdk/aws-iot/test/actions/sqs/integ.sqs-action.ts create mode 100644 packages/@aws-cdk/aws-iot/test/actions/sqs/sqs-action.test.ts diff --git a/package.json b/package.json index 1f4f938a8eb1c..e7ed2b5493d30 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,8 @@ "@aws-cdk/aws-eks/yaml/**", "@aws-cdk/aws-events-targets/aws-sdk", "@aws-cdk/aws-events-targets/aws-sdk/**", + "@aws-cdk/aws-iot/case", + "@aws-cdk/aws-iot/case/**", "@aws-cdk/aws-s3-deployment/case", "@aws-cdk/aws-s3-deployment/case/**", "@aws-cdk/cloud-assembly-schema/jsonschema", diff --git a/packages/@aws-cdk/aws-iot/NOTICE b/packages/@aws-cdk/aws-iot/NOTICE index 5fc3826926b5b..ffe28c234a860 100644 --- a/packages/@aws-cdk/aws-iot/NOTICE +++ b/packages/@aws-cdk/aws-iot/NOTICE @@ -1,2 +1,32 @@ AWS Cloud Development Kit (AWS CDK) Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +------------------------------------------------------------------------------- + +The AWS CDK includes the following third-party software/licensing: + +** case - https://www.npmjs.com/package/case +Copyright (c) 2013 Nathan Bubna + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +---------------- \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot/lib/action.ts b/packages/@aws-cdk/aws-iot/lib/action.ts index 4b218e086327e..042eeaf9d99c6 100644 --- a/packages/@aws-cdk/aws-iot/lib/action.ts +++ b/packages/@aws-cdk/aws-iot/lib/action.ts @@ -1,5 +1,5 @@ -import { IConstruct } from '@aws-cdk/core'; import { CfnTopicRule } from './iot.generated'; +import { ITopicRule } from './topic-rule-ref'; /** * An abstract action for TopicRule. @@ -10,7 +10,7 @@ export interface IAction { * * @param rule The TopicRule that would trigger this action. */ - bind(rule: IConstruct): ActionConfig; + bind(rule: ITopicRule): ActionConfig; } /** diff --git a/packages/@aws-cdk/aws-iot/lib/actions/cloudwatch-alarm-action.ts b/packages/@aws-cdk/aws-iot/lib/actions/cloudwatch-alarm-action.ts new file mode 100644 index 0000000000000..4316c2bb1202f --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/actions/cloudwatch-alarm-action.ts @@ -0,0 +1,73 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as iam from '@aws-cdk/aws-iam'; +import { IConstruct } from '@aws-cdk/core'; +import { IAction, ActionConfig } from '..'; +import { singletonActionRole } from './util'; + +/** + * Configuration properties of an action for CloudWatch alarm. + */ +export interface CloudwatchAlarmActionProps { + /** + * Reason for the alarm change. + * + * Supports substitution templates. + * @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html + * + * @default 'This state was set by the rule of AWS IoT Core.' will be set + */ + readonly stateReason?: string; + /** + * The IAM role that allows access to the CloudWatch alarm. + * + * @default a new role will be created + */ + readonly role?: iam.IRole; +} + +/** + * The action to change the state of an Amazon CloudWatch alarm. + */ +export class CloudwatchAlarmAction implements IAction { + private readonly stateReason?: string; + private readonly role?: iam.IRole; + + /** + * `stateValue` supports substitution templates. + * @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html + * + * @param alarm The CloudWatch alarm that set state by the rule + * @param stateValue The value of the alarm state. + * Valid values: OK, ALARM, INSUFFICIENT_DATA or substitution templates. + * @param props Optional properties to not use default + */ + constructor( + private readonly alarm: cloudwatch.IAlarm, + private readonly stateValue: cloudwatch.AlarmState | string, + props: CloudwatchAlarmActionProps = {}, + ) { + this.stateReason = props.stateReason; + this.role = props.role; + } + + bind(rule: IConstruct): ActionConfig { + const role = this.role ?? singletonActionRole(rule); + role.addToPrincipalPolicy(this.putEventStatement(this.alarm)); + + return { + cloudwatchAlarm: { + alarmName: this.alarm.alarmName, + stateReason: this.stateReason ?? 'This state was set by the rule of AWS IoT Core.', + stateValue: this.stateValue, + roleArn: role.roleArn, + }, + }; + } + + private putEventStatement(alarm: cloudwatch.IAlarm) { + return new iam.PolicyStatement({ + actions: ['cloudwatch:SetAlarmState'], + resources: [alarm.alarmArn], + }); + } +} diff --git a/packages/@aws-cdk/aws-iot/lib/actions/cloudwatch-logs-action.ts b/packages/@aws-cdk/aws-iot/lib/actions/cloudwatch-logs-action.ts new file mode 100644 index 0000000000000..0f23a49a32e70 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/actions/cloudwatch-logs-action.ts @@ -0,0 +1,47 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as logs from '@aws-cdk/aws-logs'; +import { IConstruct } from '@aws-cdk/core'; +import { IAction, ActionConfig } from '..'; +import { singletonActionRole } from './util'; + +/** + * Configuration properties of an action for CloudWatch Logs. + */ +export interface CloudwatchLogsActionProps { + /** + * The IAM role that allows access to the CloudWatch log group. + * + * @default a new role will be created + */ + readonly role?: iam.IRole; +} + +/** + * The action to send data to Amazon CloudWatch Logs + */ +export class CloudwatchLogsAction implements IAction { + private readonly role?: iam.IRole; + + /** + * @param logGroup The CloudWatch log group to which the action sends data + * @param props Optional properties to not use default + */ + constructor( + private readonly logGroup: logs.ILogGroup, + props: CloudwatchLogsActionProps = {}, + ) { + this.role = props.role; + } + + bind(rule: IConstruct): ActionConfig { + const role = this.role ?? singletonActionRole(rule); + this.logGroup.grant(role, 'logs:CreateLogStream', 'logs:DescribeLogStreams', 'logs:PutLogEvents'); + + return { + cloudwatchLogs: { + logGroupName: this.logGroup.logGroupName, + roleArn: role.roleArn, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot/lib/actions/cloudwatch-metric-action.ts b/packages/@aws-cdk/aws-iot/lib/actions/cloudwatch-metric-action.ts new file mode 100644 index 0000000000000..ea1d282aeee64 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/actions/cloudwatch-metric-action.ts @@ -0,0 +1,84 @@ +import * as iam from '@aws-cdk/aws-iam'; +import { IConstruct } from '@aws-cdk/core'; +import { IAction, ActionConfig } from '..'; +import { singletonActionRole } from './util'; + +/** + * Configuration properties of an action for CloudWatch metric. + */ +export interface CloudwatchMetricActionProps { + /** + * The CloudWatch metric name. + * + * Supports substitution templates. + * @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html + */ + readonly metricName: string, + /** + * The CloudWatch metric namespace name. + * + * Supports substitution templates. + * @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html + */ + readonly metricNamespace: string, + /** + * A string that contains the timestamp, expressed in seconds in Unix epoch time. + * + * Supports substitution templates. + * @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html + * + * @default None - Defaults to the current Unix epoch time. + */ + readonly metricTimestamp?: string, + /** + * The metric unit supported by CloudWatch. + * + * Supports substitution templates. + * @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html + */ + readonly metricUnit: string, + /** + * A string that contains the CloudWatch metric value. + * + * Supports substitution templates. + * @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html + */ + readonly metricValue: string, + /** + * The IAM role that allows access to the CloudWatch metric. + * + * @default a new role will be created + */ + readonly role?: iam.IRole; +} + +/** + * The action to capture an Amazon CloudWatch metric. + */ +export class CloudwatchMetricAction implements IAction { + constructor(private readonly props: CloudwatchMetricActionProps) { + } + + bind(rule: IConstruct): ActionConfig { + const role = this.props.role ?? singletonActionRole(rule); + role.addToPrincipalPolicy(this.putEventStatement()); + + return { + cloudwatchMetric: { + metricName: this.props.metricName, + metricNamespace: this.props.metricNamespace, + metricTimestamp: this.props.metricTimestamp, + metricUnit: this.props.metricUnit, + metricValue: this.props.metricValue, + roleArn: role.roleArn, + }, + }; + } + + private putEventStatement() { + return new iam.PolicyStatement({ + actions: ['cloudwatch:PutMetricData'], + resources: ['*'], + }); + } +} diff --git a/packages/@aws-cdk/aws-iot/lib/actions/dynamodb-action.ts b/packages/@aws-cdk/aws-iot/lib/actions/dynamodb-action.ts new file mode 100644 index 0000000000000..f02c6f2e08fae --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/actions/dynamodb-action.ts @@ -0,0 +1,92 @@ +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as iam from '@aws-cdk/aws-iam'; +import { IConstruct } from '@aws-cdk/core'; +import { IAction, ActionConfig } from '..'; +import { singletonActionRole } from './util'; + +/** + * Configuration properties of an action for DynamoDB. + */ +export interface DynamoDBActionProps { + /** + * The DynamoDB table. + */ + readonly table: dynamodb.Table, + /** + * The value of the partition key. + * + * Supports substitution templates. + * @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html + */ + readonly partitionKeyValue: string, + /** + * The value of the sort key. + * + * Supports substitution templates. + * @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html + */ + readonly sortKeyValue?: string, + /** + * The name of the column where the payload is written. + * + * Supports substitution templates. + * @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html + */ + readonly payloadField?: string, + /** + * The IAM role that allows access to the DynamoDB. + * + * @default a new role will be created + */ + readonly role?: iam.IRole, +} + +export enum KeyType { + STRING = 'STRING', + NUMBER = 'NUMBER', +} + +/** + * The action to write all or part of an MQTT message to an Amazon DynamoDB table. + */ +export class DynamoDBAction implements IAction { + constructor(private readonly props: DynamoDBActionProps) { + } + + bind(rule: IConstruct): ActionConfig { + const role = this.props.role ?? singletonActionRole(rule); + this.props.table.grant(role, 'dynamodb:PutItem'); + + const { partitionKey, sortKey } = this.props.table.schema(); + + return { + dynamoDb: { + tableName: this.props.table.tableName, + hashKeyField: partitionKey.name, + hashKeyType: convertToKeyType(partitionKey.type), + hashKeyValue: this.props.partitionKeyValue, + rangeKeyField: sortKey?.name, + rangeKeyType: convertToKeyType(sortKey?.type), + rangeKeyValue: this.props.sortKeyValue, + payloadField: this.props.payloadField, + roleArn: role.roleArn, + }, + }; + } +} + +function convertToKeyType(attributeType?: dynamodb.AttributeType): KeyType | undefined { + if (!attributeType) { + return; + } + + switch (attributeType) { + case dynamodb.AttributeType.STRING: + return KeyType.STRING; + case dynamodb.AttributeType.NUMBER: + return KeyType.NUMBER; + case dynamodb.AttributeType.BINARY: + throw new Error('DynamoDB Action doesn\'t support binary attribute type.'); + } + +} diff --git a/packages/@aws-cdk/aws-iot/lib/actions/dynamodb-v2-action.ts b/packages/@aws-cdk/aws-iot/lib/actions/dynamodb-v2-action.ts new file mode 100644 index 0000000000000..c0e29ba95076b --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/actions/dynamodb-v2-action.ts @@ -0,0 +1,46 @@ +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as iam from '@aws-cdk/aws-iam'; +import { IConstruct } from '@aws-cdk/core'; +import { IAction, ActionConfig } from '..'; +import { singletonActionRole } from './util'; + +/** + * Configuration properties of an action for DynamoDB. + */ +export interface DynamoDBv2ActionProps { + /** + * The IAM role that allows access to the DynamoDB. + * + * @default a new role will be created + */ + readonly role?: iam.IRole; +} + +/** + * The action to write all or part of an MQTT message to an Amazon DynamoDB table. + */ +export class DynamoDBv2Action implements IAction { + private readonly role?: iam.IRole; + + /** + * @param table The DynamoDB table to be put items by this action + * @param props Optional properties to not use default + */ + constructor(private readonly table: dynamodb.ITable, props: DynamoDBv2ActionProps = {}) { + this.role = props.role; + } + + bind(rule: IConstruct): ActionConfig { + const role = this.role ?? singletonActionRole(rule); + this.table.grant(role, 'dynamodb:PutItem'); + + return { + dynamoDBv2: { + putItem: { + tableName: this.table.tableName, + }, + roleArn: role.roleArn, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot/lib/actions/index.ts b/packages/@aws-cdk/aws-iot/lib/actions/index.ts index 4f9d659abe512..5dca2f9507bde 100644 --- a/packages/@aws-cdk/aws-iot/lib/actions/index.ts +++ b/packages/@aws-cdk/aws-iot/lib/actions/index.ts @@ -1 +1,10 @@ +export * from './cloudwatch-alarm-action'; +export * from './cloudwatch-logs-action'; +export * from './cloudwatch-metric-action'; +export * from './dynamodb-action'; +export * from './dynamodb-v2-action'; +export * from './lambda-action'; export * from './republish-action'; +export * from './s3-action'; +export * from './sns-action'; +export * from './sqs-action'; diff --git a/packages/@aws-cdk/aws-iot/lib/actions/lambda-action.ts b/packages/@aws-cdk/aws-iot/lib/actions/lambda-action.ts new file mode 100644 index 0000000000000..11f3749e53066 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/actions/lambda-action.ts @@ -0,0 +1,31 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { Stack } from '@aws-cdk/core'; +import { IAction, ActionConfig, ITopicRule } from '..'; + + +/** + * The action to invoke an AWS Lambda function, passing in an MQTT message. + */ +export class LambdaAction implements IAction { + /** + * @param lambdaFn The lambda function to be invoked by this action + */ + constructor(private readonly lambdaFn: lambda.IFunction) { + } + + bind(rule: ITopicRule): ActionConfig { + this.lambdaFn.addPermission('invokedByAwsIotRule', { + action: 'lambda:InvokeFunction', + principal: new iam.ServicePrincipal('iot.amazonaws.com'), + sourceAccount: Stack.of(rule).account, + sourceArn: rule.topicRuleArn, + }); + + return { + lambda: { + functionArn: this.lambdaFn.functionArn, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot/lib/actions/s3-action.ts b/packages/@aws-cdk/aws-iot/lib/actions/s3-action.ts new file mode 100644 index 0000000000000..daca0d57a6a05 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/actions/s3-action.ts @@ -0,0 +1,72 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import { IConstruct } from '@aws-cdk/core'; +import { kebab as toKebabCase } from 'case'; +import { IAction, ActionConfig } from '..'; +import { singletonActionRole } from './util'; + +/** + * Configuration properties of an action for s3. + */ +export interface S3ActionProps { + /** + * The Amazon S3 canned ACL that controls access to the object identified by the object key. + * @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl + */ + readonly cannedAcl?: s3.BucketAccessControl; + /** + * The path to the file where the data is written. + * + * Supports substitution templates. + * @see https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html + * + * @default '${topic()}/${timestamp()}' + */ + readonly key?: string; + /** + * The IAM role that allows access to the S3. + * + * @default a new role will be created + */ + readonly role?: iam.IRole; +} + +/** + * The action to write the data from an MQTT message to an Amazon S3 bucket. + */ +export class S3Action implements IAction { + private readonly cannedAcl?: string; + private readonly key?: string; + private readonly role?: iam.IRole; + + /** + * @param bucket The Amazon S3 bucket to which to write data. + * @param props Optional properties to not use default + */ + constructor(private readonly bucket: s3.IBucket, props: S3ActionProps = {}) { + this.cannedAcl = props.cannedAcl; + this.key = props.key; + this.role = props.role; + } + + bind(rule: IConstruct): ActionConfig { + const role = this.role ?? singletonActionRole(rule); + role.addToPrincipalPolicy(this.putEventStatement(this.bucket)); + + return { + s3: { + bucketName: this.bucket.bucketName, + cannedAcl: this.cannedAcl && toKebabCase(this.cannedAcl.toString()), + key: this.key ?? '${topic()}/${timestamp()}', + roleArn: role.roleArn, + }, + }; + } + + private putEventStatement(bucket: s3.IBucket) { + return new iam.PolicyStatement({ + actions: ['s3:PutObject'], + resources: [bucket.arnForObjects('*')], + }); + } +} diff --git a/packages/@aws-cdk/aws-iot/lib/actions/sns-action.ts b/packages/@aws-cdk/aws-iot/lib/actions/sns-action.ts new file mode 100644 index 0000000000000..b8efce09df769 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/actions/sns-action.ts @@ -0,0 +1,58 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sns from '@aws-cdk/aws-sns'; +import { IConstruct } from '@aws-cdk/core'; +import { IAction, ActionConfig } from '..'; +import { singletonActionRole } from './util'; + +/** + * Configuration properties of an action for sns. + */ +export interface SnsActionProps { + /** + * The message format. + * + * @default MessageFormat.RAW + */ + readonly messageFormat?: MessageFormat; + /** + * The IAM role that allows access to the sns topic. + * + * @default a new role will be created + */ + readonly role?: iam.IRole; +} + +export enum MessageFormat { + JSON = 'JSON', + RAW = 'RAW', +} + +/** + * The action to send the data from an MQTT message as an Amazon SNS push notification. + */ +export class SnsAction implements IAction { + private readonly messageFormat?: MessageFormat; + private readonly role?: iam.IRole; + + /** + * @param topic The SNS topic to which the push notification is sent. + * @param props Optional properties to not use default + */ + constructor(private readonly topic: sns.ITopic, props: SnsActionProps = {}) { + this.messageFormat = props.messageFormat; + this.role = props.role; + } + + bind(rule: IConstruct): ActionConfig { + const role = this.role ?? singletonActionRole(rule); + this.topic.grantPublish(role); + + return { + sns: { + messageFormat: this.messageFormat ?? MessageFormat.RAW, + roleArn: role.roleArn, + targetArn: this.topic.topicArn, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot/lib/actions/sqs-action.ts b/packages/@aws-cdk/aws-iot/lib/actions/sqs-action.ts new file mode 100644 index 0000000000000..c79d46f5385a0 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/actions/sqs-action.ts @@ -0,0 +1,54 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sqs from '@aws-cdk/aws-sqs'; +import { IConstruct } from '@aws-cdk/core'; +import { IAction, ActionConfig } from '..'; +import { singletonActionRole } from './util'; + +/** + * Configuration properties of an action for sqs. + */ +export interface SqsActionProps { + /** + * Set this parameter to true to configure the rule action to base64-encode the message + * data before it writes the data to the Amazon SQS queue. Defaults to false. + * + * @default false + */ + readonly useBase64?: boolean; + /** + * The IAM role that allows access to the sns topic. + * + * @default a new role will be created + */ + readonly role?: iam.IRole; +} + +/** + * The action to send data from an MQTT message to an Amazon SQS queue. + */ +export class SqsAction implements IAction { + private readonly useBase64?: boolean; + private readonly role?: iam.IRole; + + /** + * @param queue The URL of the Amazon SQS queue to which to write the data. + * @param props Optional properties to not use default + */ + constructor(private readonly queue: sqs.IQueue, props: SqsActionProps = {}) { + this.useBase64 = props.useBase64; + this.role = props.role; + } + + bind(rule: IConstruct): ActionConfig { + const role = this.role ?? singletonActionRole(rule); + this.queue.grant(role, 'sqs:SendMessage'); + + return { + sqs: { + queueUrl: this.queue.queueUrl, + useBase64: this.useBase64 ?? false, + roleArn: role.roleArn, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iot/lib/index.ts b/packages/@aws-cdk/aws-iot/lib/index.ts index 636ea7980e376..ff7c1541a48f1 100644 --- a/packages/@aws-cdk/aws-iot/lib/index.ts +++ b/packages/@aws-cdk/aws-iot/lib/index.ts @@ -1,5 +1,6 @@ export * from './action'; export * from './topic-rule'; +export * from './topic-rule-ref'; // AWS::IoT CloudFormation Resources: export * from './iot.generated'; diff --git a/packages/@aws-cdk/aws-iot/lib/topic-rule-ref.ts b/packages/@aws-cdk/aws-iot/lib/topic-rule-ref.ts new file mode 100644 index 0000000000000..3272c3561fe63 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/topic-rule-ref.ts @@ -0,0 +1,21 @@ +import { IResource } from '@aws-cdk/core'; + +/** + * Represents an AWS IoT Rule + */ +export interface ITopicRule extends IResource { + /** + * The value of the topic rule Amazon Resource Name (ARN), such as + * arn:aws:iot:us-east-2:123456789012:rule/rule_name + * + * @attribute + */ + readonly topicRuleArn: string; + + /** + * The name topic rule + * + * @attribute + */ + readonly topicRuleName: string; +} diff --git a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts index f467ba3bffa5e..03b6079b20952 100644 --- a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts +++ b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts @@ -1,7 +1,8 @@ -import { Lazy, Resource } from '@aws-cdk/core'; +import { ArnFormat, Lazy, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { IAction } from './action'; import { CfnTopicRule } from './iot.generated'; +import { ITopicRule } from './topic-rule-ref'; /** * The version of the SQL rules engine to use when evaluating the rule. @@ -84,7 +85,28 @@ export interface TopicRulePayloadProperty { /** * Defines an AWS IoT Rule in this stack. */ -export class TopicRule extends Resource { +export class TopicRule extends Resource implements ITopicRule { + /** + * Import an existing AWS IoT Rule provided an ARN + * + * @param scope The parent creating construct (usually `this`). + * @param id The construct's name. + * @param topicRuleArn AWS IoT Rule ARN (i.e. arn:aws:iot:::rule/MyRule). + */ + public static fromTopicRuleArn(scope: Construct, id: string, topicRuleArn: string): ITopicRule { + const parts = Stack.of(scope).splitArn(topicRuleArn, ArnFormat.SLASH_RESOURCE_NAME); + if (!parts.resourceName) { + throw new Error('Invalid topic rule arn: topicRuleArn has no resource name.'); + } + const resourceName = parts.resourceName; + + class Import extends Resource implements ITopicRule { + public readonly topicRuleArn = topicRuleArn; + public readonly topicRuleName = resourceName; + } + return new Import(scope, id); + } + /** * Arn of this rule * @attribute diff --git a/packages/@aws-cdk/aws-iot/package.json b/packages/@aws-cdk/aws-iot/package.json index edec9a0337839..3ed50a238662d 100644 --- a/packages/@aws-cdk/aws-iot/package.json +++ b/packages/@aws-cdk/aws-iot/package.json @@ -82,16 +82,34 @@ "jest": "^26.6.3" }, "dependencies": { + "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-dynamodb": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", + "case": "1.6.3", "constructs": "^3.3.69" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-dynamodb": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/aws-sqs": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^3.3.69" }, + "bundledDependencies": [ + "case" + ], "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, diff --git a/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-alarm/cloudwatch-alarm-action.test.ts b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-alarm/cloudwatch-alarm-action.test.ts new file mode 100644 index 0000000000000..8c4033c7852e9 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-alarm/cloudwatch-alarm-action.test.ts @@ -0,0 +1,256 @@ +import '@aws-cdk/assert-internal/jest'; +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { TopicRule } from '../../../lib'; +import { CloudwatchAlarmAction } from '../../../lib/actions'; + +test('Default cloudwatch alarm action', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const alarm = new cloudwatch.Alarm(stack, 'MyAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'test-namespace', + metricName: 'test-metricName', + }), + threshold: 0, + evaluationPeriods: 1, + }); + const action = new CloudwatchAlarmAction(alarm, cloudwatch.AlarmState.ALARM); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + CloudwatchAlarm: { + AlarmName: { Ref: 'MyAlarm696658B6' }, + StateValue: 'ALARM', + StateReason: 'This state was set by the rule of AWS IoT Core.', + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Default role that assumed by iot.amazonaws.com', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const alarm = new cloudwatch.Alarm(stack, 'MyAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'test-namespace', + metricName: 'test-metricName', + }), + threshold: 0, + evaluationPeriods: 1, + }); + const action = new CloudwatchAlarmAction(alarm, cloudwatch.AlarmState.ALARM); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'iot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + +test('Default policy for setting alarm state', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const alarm = new cloudwatch.Alarm(stack, 'MyAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'test-namespace', + metricName: 'test-metricName', + }), + threshold: 0, + evaluationPeriods: 1, + }); + const action = new CloudwatchAlarmAction(alarm, cloudwatch.AlarmState.ALARM); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'cloudwatch:SetAlarmState', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'MyAlarm696658B6', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [ + { Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }, + ], + }); +}); + +test('Can set state reason', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const alarm = new cloudwatch.Alarm(stack, 'MyAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'test-namespace', + metricName: 'test-metricName', + }), + threshold: 0, + evaluationPeriods: 1, + }); + const action = new CloudwatchAlarmAction(alarm, cloudwatch.AlarmState.ALARM, { + stateReason: 'test-stateReason', + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + CloudwatchAlarm: { + AlarmName: { Ref: 'MyAlarm696658B6' }, + StateValue: 'ALARM', + StateReason: 'test-stateReason', + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Can set role', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const alarm = new cloudwatch.Alarm(stack, 'MyAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'test-namespace', + metricName: 'test-metricName', + }), + threshold: 0, + evaluationPeriods: 1, + }); + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + + const action = new CloudwatchAlarmAction(alarm, cloudwatch.AlarmState.ALARM, { + role, + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + CloudwatchAlarm: { + AlarmName: { Ref: 'MyAlarm696658B6' }, + StateValue: 'ALARM', + StateReason: 'This state was set by the rule of AWS IoT Core.', + RoleArn: { + 'Fn::GetAtt': [ + 'MyRoleF48FFE04', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Add role to a policy for setting alarm state', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const alarm = new cloudwatch.Alarm(stack, 'MyAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'test-namespace', + metricName: 'test-metricName', + }), + threshold: 0, + evaluationPeriods: 1, + }); + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + + const action = new CloudwatchAlarmAction(alarm, cloudwatch.AlarmState.ALARM, { + role, + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'cloudwatch:SetAlarmState', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'MyAlarm696658B6', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyRoleDefaultPolicyA36BE1DD', + Roles: [ + { Ref: 'MyRoleF48FFE04' }, + ], + }); +}); diff --git a/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-alarm/integ.cloudwatch-alarm-action.expected.json b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-alarm/integ.cloudwatch-alarm-action.expected.json new file mode 100644 index 0000000000000..6a23b35cf9054 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-alarm/integ.cloudwatch-alarm-action.expected.json @@ -0,0 +1,86 @@ +{ + "Resources": { + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "CloudwatchAlarm": { + "AlarmName": { + "Ref": "MyAlarm696658B6" + }, + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + }, + "StateReason": "This state was set by the rule of AWS IoT Core.", + "StateValue": "ALARM" + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "RuleDisabled": false, + "Sql": "SELECT topic(2) as device_id FROM 'device/+/data'" + } + } + }, + "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": "cloudwatch:SetAlarmState", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyAlarm696658B6", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + }, + "MyAlarm696658B6": { + "Type": "AWS::CloudWatch::Alarm", + "Properties": { + "ComparisonOperator": "GreaterThanOrEqualToThreshold", + "EvaluationPeriods": 1, + "MetricName": "test", + "Namespace": "iot-test", + "Period": 300, + "Statistic": "Average", + "Threshold": 0 + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-alarm/integ.cloudwatch-alarm-action.ts b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-alarm/integ.cloudwatch-alarm-action.ts new file mode 100644 index 0000000000000..fedfb052801d9 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-alarm/integ.cloudwatch-alarm-action.ts @@ -0,0 +1,35 @@ +/// !cdk-integ pragma:ignore-assets +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as cdk from '@aws-cdk/core'; +import * as iot from '../../../lib'; +import * as actions from '../../../lib/actions'; + +const app = new cdk.App(); + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + topicRulePayload: { + sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); + + const alarm = new cloudwatch.Alarm(this, 'MyAlarm', { + metric: new cloudwatch.Metric({ + namespace: 'iot-test', + metricName: 'test', + }), + threshold: 0, + evaluationPeriods: 1, + }); + topicRule.addAction(new actions.CloudwatchAlarmAction( + alarm, + cloudwatch.AlarmState.ALARM, + )); + } +} + +new TestStack(app, 'test-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-logs/cloudwatch-logs-action.test.ts b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-logs/cloudwatch-logs-action.test.ts new file mode 100644 index 0000000000000..cd36fea497e7a --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-logs/cloudwatch-logs-action.test.ts @@ -0,0 +1,172 @@ +import '@aws-cdk/assert-internal/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import * as logs from '@aws-cdk/aws-logs'; +import * as cdk from '@aws-cdk/core'; +import { TopicRule } from '../../../lib'; +import { CloudwatchLogsAction } from '../../../lib/actions'; + +test('Default cloudwatch logs action', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); + const action = new CloudwatchLogsAction(logGroup); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + CloudwatchLogs: { + LogGroupName: { Ref: 'MyLogGroup5C0DAD85' }, + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Default role that assumed by iot.amazonaws.com', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); + const action = new CloudwatchLogsAction(logGroup); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'iot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + +test('Default policy for sending data to logs', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); + const action = new CloudwatchLogsAction(logGroup); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: ['logs:CreateLogStream', 'logs:DescribeLogStreams', 'logs:PutLogEvents'], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'MyLogGroup5C0DAD85', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [ + { Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }, + ], + }); +}); + +test('Can set role', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const action = new CloudwatchLogsAction(logGroup, { + role, + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + CloudwatchLogs: { + LogGroupName: { Ref: 'MyLogGroup5C0DAD85' }, + RoleArn: { + 'Fn::GetAtt': [ + 'MyRoleF48FFE04', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Add role to a policy for sending data to logs', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const action = new CloudwatchLogsAction(logGroup, { + role, + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: ['logs:CreateLogStream', 'logs:DescribeLogStreams', 'logs:PutLogEvents'], + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'MyLogGroup5C0DAD85', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyRoleDefaultPolicyA36BE1DD', + Roles: [ + { Ref: 'MyRoleF48FFE04' }, + ], + }); +}); diff --git a/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-logs/integ.cloudwatch-logs-action.expected.json b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-logs/integ.cloudwatch-logs-action.expected.json new file mode 100644 index 0000000000000..cd73397fc9c3d --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-logs/integ.cloudwatch-logs-action.expected.json @@ -0,0 +1,84 @@ +{ + "Resources": { + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "CloudwatchLogs": { + "LogGroupName": { + "Ref": "MyLogGroup5C0DAD85" + }, + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + } + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "RuleDisabled": false, + "Sql": "SELECT topic(2) as device_id FROM 'device/+/data'" + } + } + }, + "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": [ + "MyLogGroup5C0DAD85", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + }, + "MyLogGroup5C0DAD85": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-logs/integ.cloudwatch-logs-action.ts b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-logs/integ.cloudwatch-logs-action.ts new file mode 100644 index 0000000000000..eee7af756843a --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-logs/integ.cloudwatch-logs-action.ts @@ -0,0 +1,27 @@ +/// !cdk-integ pragma:ignore-assets +import * as logs from '@aws-cdk/aws-logs'; +import * as cdk from '@aws-cdk/core'; +import * as iot from '../../../lib'; +import * as actions from '../../../lib/actions'; + +const app = new cdk.App(); + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + topicRulePayload: { + sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); + + const logGroup = new logs.LogGroup(this, 'MyLogGroup', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + topicRule.addAction(new actions.CloudwatchLogsAction(logGroup)); + } +} + +new TestStack(app, 'test-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-metric/cloudwatch-metric-action.test.ts b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-metric/cloudwatch-metric-action.test.ts new file mode 100644 index 0000000000000..665a0635e3320 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-metric/cloudwatch-metric-action.test.ts @@ -0,0 +1,226 @@ +import '@aws-cdk/assert-internal/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { TopicRule } from '../../../lib'; +import { CloudwatchMetricAction } from '../../../lib/actions'; + +test('Default cloudwatch metric action', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'" }, + }); + + const action = new CloudwatchMetricAction({ + metricName: '${topic(2)}', + metricNamespace: '${namespace}', + metricUnit: '${unit}', + metricValue: '${value}', + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + CloudwatchMetric: { + MetricName: '${topic(2)}', + MetricNamespace: '${namespace}', + MetricUnit: '${unit}', + MetricValue: '${value}', + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'", + }, + }); +}); + +test('Default role that assumed by iot.amazonaws.com', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'" }, + }); + + const action = new CloudwatchMetricAction({ + metricName: '${topic(2)}', + metricNamespace: '${namespace}', + metricUnit: '${unit}', + metricValue: '${value}', + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'iot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + +test('Default policy for capturing metric', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'" }, + }); + + const action = new CloudwatchMetricAction({ + metricName: '${topic(2)}', + metricNamespace: '${namespace}', + metricUnit: '${unit}', + metricValue: '${value}', + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'cloudwatch:PutMetricData', + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [ + { Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }, + ], + }); +}); + +test('Can set timestamp', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'" }, + }); + + const action = new CloudwatchMetricAction({ + metricName: '${topic(2)}', + metricNamespace: '${namespace}', + metricUnit: '${unit}', + metricValue: '${value}', + metricTimestamp: '${timestamp}', + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + CloudwatchMetric: { + MetricName: '${topic(2)}', + MetricNamespace: '${namespace}', + MetricUnit: '${unit}', + MetricValue: '${value}', + MetricTimestamp: '${timestamp}', + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'", + }, + }); +}); + +test('Can set role', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'" }, + }); + + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const action = new CloudwatchMetricAction({ + metricName: '${topic(2)}', + metricNamespace: '${namespace}', + metricUnit: '${unit}', + metricValue: '${value}', + role, + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + CloudwatchMetric: { + MetricName: '${topic(2)}', + MetricNamespace: '${namespace}', + MetricUnit: '${unit}', + MetricValue: '${value}', + RoleArn: { + 'Fn::GetAtt': [ + 'MyRoleF48FFE04', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'", + }, + }); +}); + +test('Add role to a policy for capturing metric', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'" }, + }); + + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const action = new CloudwatchMetricAction({ + metricName: '${topic(2)}', + metricNamespace: '${namespace}', + metricUnit: '${unit}', + metricValue: '${value}', + role, + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'cloudwatch:PutMetricData', + Effect: 'Allow', + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyRoleDefaultPolicyA36BE1DD', + Roles: [ + { Ref: 'MyRoleF48FFE04' }, + ], + }); +}); diff --git a/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-metric/integ.cloudwatch-metric-action.expected.json b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-metric/integ.cloudwatch-metric-action.expected.json new file mode 100644 index 0000000000000..f9fd4a515a7d4 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-metric/integ.cloudwatch-metric-action.expected.json @@ -0,0 +1,69 @@ +{ + "Resources": { + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "CloudwatchMetric": { + "MetricName": "${topic(2)}", + "MetricNamespace": "${namespace}", + "MetricTimestamp": "${timestamp}", + "MetricUnit": "${unit}", + "MetricValue": "${value}", + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + } + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "RuleDisabled": false, + "Sql": "SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'" + } + } + }, + "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": "cloudwatch:PutMetricData", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-metric/integ.cloudwatch-metric-action.ts b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-metric/integ.cloudwatch-metric-action.ts new file mode 100644 index 0000000000000..64175aecad761 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/cloudwatch-metric/integ.cloudwatch-metric-action.ts @@ -0,0 +1,29 @@ +/// !cdk-integ pragma:ignore-assets +import * as cdk from '@aws-cdk/core'; +import * as iot from '../../../lib'; +import * as actions from '../../../lib/actions'; + +const app = new cdk.App(); + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + topicRulePayload: { + sql: "SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'", + }, + }); + + topicRule.addAction(new actions.CloudwatchMetricAction({ + metricName: '${topic(2)}', + metricNamespace: '${namespace}', + metricUnit: '${unit}', + metricValue: '${value}', + metricTimestamp: '${timestamp}', + })); + } +} + +new TestStack(app, 'test-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iot/test/actions/dynamodb-v2/dynamodb-v2-action.test.ts b/packages/@aws-cdk/aws-iot/test/actions/dynamodb-v2/dynamodb-v2-action.test.ts new file mode 100644 index 0000000000000..f423859fb2341 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/dynamodb-v2/dynamodb-v2-action.test.ts @@ -0,0 +1,192 @@ +import '@aws-cdk/assert-internal/jest'; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { TopicRule } from '../../../lib'; +import { DynamoDBv2Action } from '../../../lib/actions'; + +test('Default dynamodb v2 action', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { type: dynamodb.AttributeType.STRING, name: 'device_id' }, + }); + const action = new DynamoDBv2Action(table); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + DynamoDBv2: { + PutItem: { + TableName: { Ref: 'MyTable794EDED1' }, + }, + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Default role that assumed by iot.amazonaws.com', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { type: dynamodb.AttributeType.STRING, name: 'device_id' }, + }); + const action = new DynamoDBv2Action(table); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'iot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + +test('Default policy for putting a item to DynamoDB', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { type: dynamodb.AttributeType.STRING, name: 'device_id' }, + }); + const action = new DynamoDBv2Action(table); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'dynamodb:PutItem', + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': [ + 'MyTable794EDED1', + 'Arn', + ], + }, + { Ref: 'AWS::NoValue' }, + ], + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [ + { Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }, + ], + }); +}); + +test('Can set role', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { type: dynamodb.AttributeType.STRING, name: 'pKey' }, + }); + const action = new DynamoDBv2Action(table, { + role, + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + DynamoDBv2: { + PutItem: { + TableName: { Ref: 'MyTable794EDED1' }, + }, + RoleArn: { + 'Fn::GetAtt': [ + 'MyRoleF48FFE04', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Add role to a policy for putting a item to DynamoDB', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { type: dynamodb.AttributeType.STRING, name: 'pKey' }, + }); + const action = new DynamoDBv2Action(table, { + role, + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'dynamodb:PutItem', + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': [ + 'MyTable794EDED1', + 'Arn', + ], + }, + { Ref: 'AWS::NoValue' }, + ], + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyRoleDefaultPolicyA36BE1DD', + Roles: [ + { Ref: 'MyRoleF48FFE04' }, + ], + }); +}); diff --git a/packages/@aws-cdk/aws-iot/test/actions/dynamodb-v2/integ.dynamodb-v2-action.expected.json b/packages/@aws-cdk/aws-iot/test/actions/dynamodb-v2/integ.dynamodb-v2-action.expected.json new file mode 100644 index 0000000000000..079b7a5b3184d --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/dynamodb-v2/integ.dynamodb-v2-action.expected.json @@ -0,0 +1,110 @@ +{ + "Resources": { + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "DynamoDBv2": { + "PutItem": { + "TableName": { + "Ref": "MyTable794EDED1" + } + }, + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + } + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "RuleDisabled": false, + "Sql": "SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'" + } + } + }, + "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": "dynamodb:PutItem", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyTable794EDED1", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + }, + "MyTable794EDED1": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "device_id", + "KeyType": "HASH" + }, + { + "AttributeName": "timestamp", + "KeyType": "RANGE" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "device_id", + "AttributeType": "S" + }, + { + "AttributeName": "timestamp", + "AttributeType": "N" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot/test/actions/dynamodb-v2/integ.dynamodb-v2-action.ts b/packages/@aws-cdk/aws-iot/test/actions/dynamodb-v2/integ.dynamodb-v2-action.ts new file mode 100644 index 0000000000000..1dd965a665481 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/dynamodb-v2/integ.dynamodb-v2-action.ts @@ -0,0 +1,29 @@ +/// !cdk-integ pragma:ignore-assets +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as cdk from '@aws-cdk/core'; +import * as iot from '../../../lib'; +import * as actions from '../../../lib/actions'; + +const app = new cdk.App(); + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + topicRulePayload: { + sql: "SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'", + }, + }); + + const table = new dynamodb.Table(this, 'MyTable', { + partitionKey: { type: dynamodb.AttributeType.STRING, name: 'device_id' }, + sortKey: { type: dynamodb.AttributeType.NUMBER, name: 'timestamp' }, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + topicRule.addAction(new actions.DynamoDBv2Action(table)); + } +} + +new TestStack(app, 'test-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iot/test/actions/dynamodb/dynamodb-action.test.ts b/packages/@aws-cdk/aws-iot/test/actions/dynamodb/dynamodb-action.test.ts new file mode 100644 index 0000000000000..3946ce6d6bdbf --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/dynamodb/dynamodb-action.test.ts @@ -0,0 +1,274 @@ +import '@aws-cdk/assert-internal/jest'; +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { TopicRule } from '../../../lib'; +import { DynamoDBAction } from '../../../lib/actions'; + +test('Default dynamodb action', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { type: dynamodb.AttributeType.STRING, name: 'pKey' }, + }); + const action = new DynamoDBAction({ + table, + partitionKeyValue: '${topic(2)}', + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + DynamoDB: { + TableName: { Ref: 'MyTable794EDED1' }, + HashKeyField: 'pKey', + HashKeyType: 'STRING', + HashKeyValue: '${topic(2)}', + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Default role that assumed by iot.amazonaws.com', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { type: dynamodb.AttributeType.STRING, name: 'pKey' }, + }); + const action = new DynamoDBAction({ + table, + partitionKeyValue: '${topic(2)}', + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'iot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + +test('Default policy for putting a item to DynamoDB', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { type: dynamodb.AttributeType.STRING, name: 'pKey' }, + }); + const action = new DynamoDBAction({ + table, + partitionKeyValue: '${topic(2)}', + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'dynamodb:PutItem', + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': [ + 'MyTable794EDED1', + 'Arn', + ], + }, + { Ref: 'AWS::NoValue' }, + ], + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [ + { Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }, + ], + }); +}); + +test('Can not set a table that have binary attribute type key', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id, timestamp() as timestamp FROM 'device/+/data'" }, + }); + + const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { type: dynamodb.AttributeType.BINARY, name: 'pKey' }, + }); + + const action = new DynamoDBAction({ + table, + partitionKeyValue: '${topic(2)}', + }); + + expect(() => topicRule.addAction(action)).toThrowError( + 'DynamoDB Action doesn\'t support binary attribute type.', + ); +}); + +test('Can set optional properties', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id, timestamp() as timestamp FROM 'device/+/data'" }, + }); + + const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { type: dynamodb.AttributeType.STRING, name: 'pKey' }, + sortKey: { type: dynamodb.AttributeType.NUMBER, name: 'sKey' }, + }); + const action = new DynamoDBAction({ + table, + partitionKeyValue: '${topic(2)}', + sortKeyValue: '${timestamp()}', + payloadField: 'custom-payload-field', + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + DynamoDB: { + TableName: { Ref: 'MyTable794EDED1' }, + HashKeyField: 'pKey', + HashKeyType: 'STRING', + HashKeyValue: '${topic(2)}', + RangeKeyField: 'sKey', + RangeKeyType: 'NUMBER', + RangeKeyValue: '${timestamp()}', + PayloadField: 'custom-payload-field', + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id, timestamp() as timestamp FROM 'device/+/data'", + }, + }); +}); + +test('Can set role', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { type: dynamodb.AttributeType.STRING, name: 'pKey' }, + }); + const action = new DynamoDBAction({ + table, + partitionKeyValue: '${topic(2)}', + role, + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + DynamoDB: { + TableName: { Ref: 'MyTable794EDED1' }, + HashKeyField: 'pKey', + HashKeyType: 'STRING', + HashKeyValue: '${topic(2)}', + RoleArn: { + 'Fn::GetAtt': [ + 'MyRoleF48FFE04', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Add role to a policy for putting a item to DynamoDB', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const table = new dynamodb.Table(stack, 'MyTable', { + partitionKey: { type: dynamodb.AttributeType.STRING, name: 'pKey' }, + }); + const action = new DynamoDBAction({ + table, + partitionKeyValue: '${topic(2)}', + role, + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'dynamodb:PutItem', + Effect: 'Allow', + Resource: [ + { + 'Fn::GetAtt': [ + 'MyTable794EDED1', + 'Arn', + ], + }, + { Ref: 'AWS::NoValue' }, + ], + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyRoleDefaultPolicyA36BE1DD', + Roles: [ + { Ref: 'MyRoleF48FFE04' }, + ], + }); +}); diff --git a/packages/@aws-cdk/aws-iot/test/actions/dynamodb/integ.dynamodb-action.expected.json b/packages/@aws-cdk/aws-iot/test/actions/dynamodb/integ.dynamodb-action.expected.json new file mode 100644 index 0000000000000..49f41403d0c52 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/dynamodb/integ.dynamodb-action.expected.json @@ -0,0 +1,115 @@ +{ + "Resources": { + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "DynamoDB": { + "HashKeyField": "device_id", + "HashKeyType": "STRING", + "HashKeyValue": "${topic(2)}", + "PayloadField": "custom-payload-field", + "RangeKeyField": "timestamp", + "RangeKeyType": "NUMBER", + "RangeKeyValue": "${timestamp()}", + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + }, + "TableName": { + "Ref": "MyTable794EDED1" + } + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "RuleDisabled": false, + "Sql": "SELECT topic(2) as device_id FROM 'device/+/data'" + } + } + }, + "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": "dynamodb:PutItem", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "MyTable794EDED1", + "Arn" + ] + }, + { + "Ref": "AWS::NoValue" + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + }, + "MyTable794EDED1": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "device_id", + "KeyType": "HASH" + }, + { + "AttributeName": "timestamp", + "KeyType": "RANGE" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "device_id", + "AttributeType": "S" + }, + { + "AttributeName": "timestamp", + "AttributeType": "N" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot/test/actions/dynamodb/integ.dynamodb-action.ts b/packages/@aws-cdk/aws-iot/test/actions/dynamodb/integ.dynamodb-action.ts new file mode 100644 index 0000000000000..aabc6e9d8aae4 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/dynamodb/integ.dynamodb-action.ts @@ -0,0 +1,34 @@ +/// !cdk-integ pragma:ignore-assets +import * as dynamodb from '@aws-cdk/aws-dynamodb'; +import * as cdk from '@aws-cdk/core'; +import * as iot from '../../../lib'; +import * as actions from '../../../lib/actions'; + +const app = new cdk.App(); + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + topicRulePayload: { + sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); + + const table = new dynamodb.Table(this, 'MyTable', { + partitionKey: { type: dynamodb.AttributeType.STRING, name: 'device_id' }, + sortKey: { type: dynamodb.AttributeType.NUMBER, name: 'timestamp' }, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + topicRule.addAction(new actions.DynamoDBAction({ + table, + partitionKeyValue: '${topic(2)}', + sortKeyValue: '${timestamp()}', + payloadField: 'custom-payload-field', + })); + } +} + +new TestStack(app, 'test-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iot/test/actions/lambda/integ.lambda-action.expected.json b/packages/@aws-cdk/aws-iot/test/actions/lambda/integ.lambda-action.expected.json new file mode 100644 index 0000000000000..dced752b51d48 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/lambda/integ.lambda-action.expected.json @@ -0,0 +1,98 @@ +{ + "Resources": { + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Lambda": { + "FunctionArn": { + "Fn::GetAtt": [ + "MyFunction3BAA72D1", + "Arn" + ] + } + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "RuleDisabled": false, + "Sql": "SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'" + } + } + }, + "MyFunctionServiceRole3C357FF2": { + "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" + ] + ] + } + ] + } + }, + "MyFunction3BAA72D1": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "\nexports.handler = (event) => {\n console.log(\"It is test for lambda action of AWS IoT Rule.\", event)\n}\n" + }, + "Role": { + "Fn::GetAtt": [ + "MyFunctionServiceRole3C357FF2", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x" + }, + "DependsOn": [ + "MyFunctionServiceRole3C357FF2" + ] + }, + "MyFunctioninvokedByAwsIotRule5581F304": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyFunction3BAA72D1", + "Arn" + ] + }, + "Principal": "iot.amazonaws.com", + "SourceAccount": { + "Ref": "AWS::AccountId" + }, + "SourceArn": { + "Fn::GetAtt": [ + "TopicRule40A4EA44", + "Arn" + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot/test/actions/lambda/integ.lambda-action.ts b/packages/@aws-cdk/aws-iot/test/actions/lambda/integ.lambda-action.ts new file mode 100644 index 0000000000000..fb3211fd27aa8 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/lambda/integ.lambda-action.ts @@ -0,0 +1,33 @@ +/// !cdk-integ pragma:ignore-assets +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import * as iot from '../../../lib'; +import * as actions from '../../../lib/actions'; + +const app = new cdk.App(); + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + topicRulePayload: { + sql: "SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'", + }, + }); + + const lambdaFn = new lambda.Function(this, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromInline(` +exports.handler = (event) => { + console.log("It is test for lambda action of AWS IoT Rule.", event) +} +`), + }); + topicRule.addAction(new actions.LambdaAction(lambdaFn)); + } +} + +new TestStack(app, 'test-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iot/test/actions/lambda/lambda-action.test.ts b/packages/@aws-cdk/aws-iot/test/actions/lambda/lambda-action.test.ts new file mode 100644 index 0000000000000..139248aa6a54b --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/lambda/lambda-action.test.ts @@ -0,0 +1,73 @@ +import '@aws-cdk/assert-internal/jest'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as cdk from '@aws-cdk/core'; +import { TopicRule } from '../../../lib'; +import { LambdaAction } from '../../../lib/actions'; + +test('Have topic rule resource that include lambda action', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const lambdaFn = new lambda.Function(stack, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromInline('console.log("foo")'), + }); + const action = new LambdaAction(lambdaFn); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Lambda: { + FunctionArn: { + 'Fn::GetAtt': [ + 'MyFunction3BAA72D1', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Have lambda permission resource to invoked by IoT Rule', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const lambdaFn = new lambda.Function(stack, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + code: lambda.Code.fromInline('console.log("foo")'), + }); + const action = new LambdaAction(lambdaFn); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + 'Fn::GetAtt': [ + 'MyFunction3BAA72D1', + 'Arn', + ], + }, + Principal: 'iot.amazonaws.com', + SourceAccount: { Ref: 'AWS::AccountId' }, + SourceArn: { + 'Fn::GetAtt': [ + 'MyTopicRule4EC2091C', + 'Arn', + ], + }, + }); +}); diff --git a/packages/@aws-cdk/aws-iot/test/actions/s3/integ.s3-action.expected.json b/packages/@aws-cdk/aws-iot/test/actions/s3/integ.s3-action.expected.json new file mode 100644 index 0000000000000..f5b947ecc201b --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/s3/integ.s3-action.expected.json @@ -0,0 +1,121 @@ +{ + "Resources": { + "logs0B6081B1": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "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": "s3:PutObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyBucketF68F3FF0", + "Arn" + ] + }, + "/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + }, + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "S3": { + "BucketName": { + "Ref": "MyBucketF68F3FF0" + }, + "Key": "${topic()}/${timestamp()}", + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + } + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "ErrorAction": { + "CloudwatchLogs": { + "LogGroupName": { + "Ref": "logs0B6081B1" + }, + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + } + } + }, + "RuleDisabled": false, + "Sql": "SELECT topic(2) as device_id FROM 'device/+/data'" + } + } + }, + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot/test/actions/s3/integ.s3-action.ts b/packages/@aws-cdk/aws-iot/test/actions/s3/integ.s3-action.ts new file mode 100644 index 0000000000000..425b599262008 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/s3/integ.s3-action.ts @@ -0,0 +1,29 @@ +/// !cdk-integ pragma:ignore-assets +import * as logs from '@aws-cdk/aws-logs'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as iot from '../../../lib'; +import * as actions from '../../../lib/actions'; + +const app = new cdk.App(); + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + topicRulePayload: { + sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + errorAction: new actions.CloudwatchLogsAction(new logs.LogGroup(this, 'logs')), + }, + }); + + const bucket = new s3.Bucket(this, 'MyBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + topicRule.addAction(new actions.S3Action(bucket)); + } +} + +new TestStack(app, 'test-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iot/test/actions/s3/s3-action.test.ts b/packages/@aws-cdk/aws-iot/test/actions/s3/s3-action.test.ts new file mode 100644 index 0000000000000..13ce100007e21 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/s3/s3-action.test.ts @@ -0,0 +1,257 @@ +import '@aws-cdk/assert-internal/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import { TopicRule } from '../../../lib'; +import { S3Action } from '../../../lib/actions'; + +test('Default s3 action', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const bucket = new s3.Bucket(stack, 'MyBucket'); + const action = new S3Action(bucket); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + S3: { + BucketName: { Ref: 'MyBucketF68F3FF0' }, + Key: '${topic()}/${timestamp()}', + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Default role that assumed by iot.amazonaws.com', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const bucket = new s3.Bucket(stack, 'MyBucket'); + const action = new S3Action(bucket); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'iot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + +test('Default policy for putting item to a bucket', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const bucket = new s3.Bucket(stack, 'MyBucket'); + const action = new S3Action(bucket); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 's3:PutObject', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'MyBucketF68F3FF0', + 'Arn', + ], + }, + '/*', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [ + { Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }, + ], + }); +}); + +test('Can set key of bucket', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const bucket = new s3.Bucket(stack, 'MyBucket'); + const action = new S3Action(bucket, { + key: 'test-key', + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + S3: { + BucketName: { Ref: 'MyBucketF68F3FF0' }, + Key: 'test-key', + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Can set canned ACL and it convert to kebab case', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const bucket = new s3.Bucket(stack, 'MyBucket'); + const action = new S3Action(bucket, { + cannedAcl: s3.BucketAccessControl.BUCKET_OWNER_FULL_CONTROL, + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + S3: { + BucketName: { Ref: 'MyBucketF68F3FF0' }, + Key: '${topic()}/${timestamp()}', + CannedAcl: 'bucket-owner-full-control', + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Can set role', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const bucket = new s3.Bucket(stack, 'MyBucket'); + const action = new S3Action(bucket, { role }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + S3: { + BucketName: { Ref: 'MyBucketF68F3FF0' }, + Key: '${topic()}/${timestamp()}', + RoleArn: { + 'Fn::GetAtt': [ + 'MyRoleF48FFE04', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Add role to a policy for putting item to a bucket', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const bucket = new s3.Bucket(stack, 'MyBucket'); + const action = new S3Action(bucket, { role }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 's3:PutObject', + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'MyBucketF68F3FF0', + 'Arn', + ], + }, + '/*', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyRoleDefaultPolicyA36BE1DD', + Roles: [ + { Ref: 'MyRoleF48FFE04' }, + ], + }); +}); diff --git a/packages/@aws-cdk/aws-iot/test/actions/sns/integ.sns-action.expected.json b/packages/@aws-cdk/aws-iot/test/actions/sns/integ.sns-action.expected.json new file mode 100644 index 0000000000000..4e1b287391cff --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/sns/integ.sns-action.expected.json @@ -0,0 +1,108 @@ +{ + "Resources": { + "logs0B6081B1": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "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": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "MyTopic86869434" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + }, + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Sns": { + "MessageFormat": "RAW", + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + }, + "TargetArn": { + "Ref": "MyTopic86869434" + } + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "ErrorAction": { + "CloudwatchLogs": { + "LogGroupName": { + "Ref": "logs0B6081B1" + }, + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + } + } + }, + "RuleDisabled": false, + "Sql": "SELECT topic(2) as device_id FROM 'device/+/data'" + } + } + }, + "MyTopic86869434": { + "Type": "AWS::SNS::Topic" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot/test/actions/sns/integ.sns-action.ts b/packages/@aws-cdk/aws-iot/test/actions/sns/integ.sns-action.ts new file mode 100644 index 0000000000000..8b14f020e679e --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/sns/integ.sns-action.ts @@ -0,0 +1,27 @@ +/// !cdk-integ pragma:ignore-assets +import * as logs from '@aws-cdk/aws-logs'; +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import * as iot from '../../../lib'; +import * as actions from '../../../lib/actions'; + +const app = new cdk.App(); + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + topicRulePayload: { + sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + errorAction: new actions.CloudwatchLogsAction(new logs.LogGroup(this, 'logs')), + }, + }); + + const topic = new sns.Topic(this, 'MyTopic'); + topicRule.addAction(new actions.SnsAction(topic)); + } +} + +new TestStack(app, 'test-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iot/test/actions/sns/sns-action.test.ts b/packages/@aws-cdk/aws-iot/test/actions/sns/sns-action.test.ts new file mode 100644 index 0000000000000..5f9109d29c8d8 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/sns/sns-action.test.ts @@ -0,0 +1,195 @@ +import '@aws-cdk/assert-internal/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import { TopicRule } from '../../../lib'; +import { MessageFormat, SnsAction } from '../../../lib/actions'; + +test('Default SNS action', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const topic = new sns.Topic(stack, 'MyTopic'); + const action = new SnsAction(topic); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Sns: { + MessageFormat: 'RAW', + TargetArn: { Ref: 'MyTopic86869434' }, + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Default role that assumed by iot.amazonaws.com', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const topic = new sns.Topic(stack, 'MyTopic'); + const action = new SnsAction(topic); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'iot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + +test('Default policy for sending sns topic', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const topic = new sns.Topic(stack, 'MyTopic'); + const action = new SnsAction(topic); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'sns:Publish', + Effect: 'Allow', + Resource: { Ref: 'MyTopic86869434' }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [ + { Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }, + ], + }); +}); + +test('Can set message format', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const topic = new sns.Topic(stack, 'MyTopic'); + const action = new SnsAction(topic, { + messageFormat: MessageFormat.JSON, + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Sns: { + MessageFormat: 'JSON', + TargetArn: { Ref: 'MyTopic86869434' }, + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Can set role', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const topic = new sns.Topic(stack, 'MyTopic'); + const action = new SnsAction(topic, { role }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Sns: { + MessageFormat: 'RAW', + TargetArn: { Ref: 'MyTopic86869434' }, + RoleArn: { + 'Fn::GetAtt': [ + 'MyRoleF48FFE04', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Add role to a policy for sending sns topic', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const topic = new sns.Topic(stack, 'MyTopic'); + const action = new SnsAction(topic, { role }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'sns:Publish', + Effect: 'Allow', + Resource: { Ref: 'MyTopic86869434' }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyRoleDefaultPolicyA36BE1DD', + Roles: [ + { Ref: 'MyRoleF48FFE04' }, + ], + }); +}); diff --git a/packages/@aws-cdk/aws-iot/test/actions/sqs/integ.sqs-action.expected.json b/packages/@aws-cdk/aws-iot/test/actions/sqs/integ.sqs-action.expected.json new file mode 100644 index 0000000000000..6a017118ef8e5 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/sqs/integ.sqs-action.expected.json @@ -0,0 +1,113 @@ +{ + "Resources": { + "logs0B6081B1": { + "Type": "AWS::Logs::LogGroup", + "Properties": { + "RetentionInDays": 731 + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "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": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyQueueE6CA6235", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "TopicRuleTopicRuleActionRoleDefaultPolicy99ADD687", + "Roles": [ + { + "Ref": "TopicRuleTopicRuleActionRole246C4F77" + } + ] + } + }, + "TopicRule40A4EA44": { + "Type": "AWS::IoT::TopicRule", + "Properties": { + "TopicRulePayload": { + "Actions": [ + { + "Sqs": { + "QueueUrl": { + "Ref": "MyQueueE6CA6235" + }, + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + }, + "UseBase64": false + } + } + ], + "AwsIotSqlVersion": "2015-10-08", + "ErrorAction": { + "CloudwatchLogs": { + "LogGroupName": { + "Ref": "logs0B6081B1" + }, + "RoleArn": { + "Fn::GetAtt": [ + "TopicRuleTopicRuleActionRole246C4F77", + "Arn" + ] + } + } + }, + "RuleDisabled": false, + "Sql": "SELECT topic(2) as device_id FROM 'device/+/data'" + } + } + }, + "MyQueueE6CA6235": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iot/test/actions/sqs/integ.sqs-action.ts b/packages/@aws-cdk/aws-iot/test/actions/sqs/integ.sqs-action.ts new file mode 100644 index 0000000000000..4ba9d6871935e --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/sqs/integ.sqs-action.ts @@ -0,0 +1,29 @@ +/// !cdk-integ pragma:ignore-assets +import * as logs from '@aws-cdk/aws-logs'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as cdk from '@aws-cdk/core'; +import * as iot from '../../../lib'; +import * as actions from '../../../lib/actions'; + +const app = new cdk.App(); + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const topicRule = new iot.TopicRule(this, 'TopicRule', { + topicRulePayload: { + sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + errorAction: new actions.CloudwatchLogsAction(new logs.LogGroup(this, 'logs')), + }, + }); + + const queue = new sqs.Queue(this, 'MyQueue', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + topicRule.addAction(new actions.SqsAction(queue)); + } +} + +new TestStack(app, 'test-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iot/test/actions/sqs/sqs-action.test.ts b/packages/@aws-cdk/aws-iot/test/actions/sqs/sqs-action.test.ts new file mode 100644 index 0000000000000..305939adba1cd --- /dev/null +++ b/packages/@aws-cdk/aws-iot/test/actions/sqs/sqs-action.test.ts @@ -0,0 +1,205 @@ +import '@aws-cdk/assert-internal/jest'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sqs from '@aws-cdk/aws-sqs'; +import * as cdk from '@aws-cdk/core'; +import { TopicRule } from '../../../lib'; +import { SqsAction } from '../../../lib/actions'; + +test('Default SQS action', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const queue = new sqs.Queue(stack, 'MyQueue'); + const action = new SqsAction(queue); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Sqs: { + QueueUrl: { Ref: 'MyQueueE6CA6235' }, + UseBase64: false, + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Default role that assumed by iot.amazonaws.com', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const queue = new sqs.Queue(stack, 'MyQueue'); + const action = new SqsAction(queue); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'iot.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + +test('Default policy for sending message to SQS Queue', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const queue = new sqs.Queue(stack, 'MyQueue'); + const action = new SqsAction(queue); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'sqs:SendMessage', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyTopicRuleTopicRuleActionRoleDefaultPolicy54A701F7', + Roles: [ + { Ref: 'MyTopicRuleTopicRuleActionRoleCE2D05DA' }, + ], + }); +}); + +test('Can set useBase64', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const queue = new sqs.Queue(stack, 'MyQueue'); + const action = new SqsAction(queue, { + useBase64: true, + }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Sqs: { + QueueUrl: { Ref: 'MyQueueE6CA6235' }, + UseBase64: true, + RoleArn: { + 'Fn::GetAtt': [ + 'MyTopicRuleTopicRuleActionRoleCE2D05DA', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Can set role', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const queue = new sqs.Queue(stack, 'MyQueue'); + const action = new SqsAction(queue, { role }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Sqs: { + QueueUrl: { Ref: 'MyQueueE6CA6235' }, + UseBase64: false, + RoleArn: { + 'Fn::GetAtt': [ + 'MyRoleF48FFE04', + 'Arn', + ], + }, + }, + }, + ], + AwsIotSqlVersion: '2015-10-08', + RuleDisabled: false, + Sql: "SELECT topic(2) as device_id FROM 'device/+/data'", + }, + }); +}); + +test('Add role to a policy for sending message to SQS Queue', () => { + const stack = new cdk.Stack(); + const topicRule = new TopicRule(stack, 'MyTopicRule', { + topicRulePayload: { sql: "SELECT topic(2) as device_id FROM 'device/+/data'" }, + }); + + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'), + }); + const queue = new sqs.Queue(stack, 'MyQueue'); + const action = new SqsAction(queue, { role }); + topicRule.addAction(action); + + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'sqs:SendMessage', + Effect: 'Allow', + Resource: { + 'Fn::GetAtt': [ + 'MyQueueE6CA6235', + 'Arn', + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'MyRoleDefaultPolicyA36BE1DD', + Roles: [ + { Ref: 'MyRoleF48FFE04' }, + ], + }); +}); 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 5f646dc922216..8a629e36aba82 100644 --- a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts +++ b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts @@ -300,3 +300,26 @@ test('Can not add actions have multiple action properties', () => { 'Each object in the actions list can only have one action defined. keys: http,lambda', ); }); + +test('Can import from topic rule arn', () => { + const stack = new cdk.Stack(); + + const topicRuleArn = 'arn:aws:iot:ap-northeast-1:123456789012:rule/my-rule-name'; + + const topicRule = TopicRule.fromTopicRuleArn(stack, 'TopicRuleFromArn', topicRuleArn); + + expect(topicRule).toMatchObject({ + topicRuleArn, + topicRuleName: 'my-rule-name', + }); +}); + +test('Can not import if invalid topic rule arn', () => { + const stack = new cdk.Stack(); + + const topicRuleArn = 'arn:aws:iot:ap-northeast-1:123456789012:rule/'; + + expect(() => TopicRule.fromTopicRuleArn(stack, 'TopicRuleFromArn', topicRuleArn)).toThrowError( + 'Invalid topic rule arn: topicRuleArn has no resource name.', + ); +});