From 7a72b32d40bb06f899b1d6fbeb14f034bdfe0254 Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Fri, 22 Oct 2021 13:22:28 +0900 Subject: [PATCH] feat(iot): Add IAction This change adds `IAction` to aws-iot for preparing to implemamt AWS IoT Rule actions. --- packages/@aws-cdk/aws-iot/README.md | 36 +++++- packages/@aws-cdk/aws-iot/lib/action.ts | 24 ++++ packages/@aws-cdk/aws-iot/lib/index.ts | 1 + packages/@aws-cdk/aws-iot/lib/topic-rule.ts | 37 +++++- .../test/integ.topic-rule.expected.json | 8 +- .../@aws-cdk/aws-iot/test/integ.topic-rule.ts | 9 ++ .../@aws-cdk/aws-iot/test/topic-rule.test.ts | 113 ++++++++++++++++++ 7 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 packages/@aws-cdk/aws-iot/lib/action.ts diff --git a/packages/@aws-cdk/aws-iot/README.md b/packages/@aws-cdk/aws-iot/README.md index bbde9aae8a21d..a4627a5b444f8 100644 --- a/packages/@aws-cdk/aws-iot/README.md +++ b/packages/@aws-cdk/aws-iot/README.md @@ -40,16 +40,44 @@ import * as iot from '@aws-cdk/aws-iot'; ## `TopicRule` -The `TopicRule` construct defined Rules that give your devices the ability to -interact with AWS services. - -For example, to define a rule: +Create a rule that give your devices the ability to interact with AWS services. +You can create a rule with an action that invoke the Lambda action as following: ```ts +import * as iot from '@aws-cdk/aws-iot'; + new iot.TopicRule(stack, 'MyTopicRule', { topicRuleName: 'MyRuleName', // optional property sql: iot.IotSql.fromStringAsVer20160323( "SELECT topic(2) as device_id, temperature FROM 'device/+/data'", ), + actions: [ + { + bind: () => ({ + configuration: { + lambda: { functionArn: 'test-functionArn' }, + }, + }), + }, + ], +}); +``` + +Or, you can add an action after constructing the `TopicRule` instance as following: + +```ts +import * as iot from '@aws-cdk/aws-iot'; + +const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20160323( + "SELECT topic(2) as device_id, temperature FROM 'device/+/data'", + ), }); +topicRule.addAction({ + bind: () => ({ + configuration: { + lambda: { functionArn: 'test-functionArn' }, + }, + }), +}) ``` diff --git a/packages/@aws-cdk/aws-iot/lib/action.ts b/packages/@aws-cdk/aws-iot/lib/action.ts new file mode 100644 index 0000000000000..a21168e479e55 --- /dev/null +++ b/packages/@aws-cdk/aws-iot/lib/action.ts @@ -0,0 +1,24 @@ +import { CfnTopicRule } from './iot.generated'; +import { ITopicRule } from './topic-rule'; + +/** + * An abstract action for TopicRule. + */ +export interface IAction { + /** + * Returns the topic rule action specification. + * + * @param rule The TopicRule that would trigger this action. + */ + bind(rule: ITopicRule): ActionConfig; +} + +/** + * Properties for an topic rule action + */ +export interface ActionConfig { + /** + * The configuration for this action. + */ + readonly configuration: CfnTopicRule.ActionProperty; +} diff --git a/packages/@aws-cdk/aws-iot/lib/index.ts b/packages/@aws-cdk/aws-iot/lib/index.ts index 18b6f2e03aaeb..f2e82a6c755b2 100644 --- a/packages/@aws-cdk/aws-iot/lib/index.ts +++ b/packages/@aws-cdk/aws-iot/lib/index.ts @@ -1,3 +1,4 @@ +export * from './action'; export * from './iot-sql'; export * from './topic-rule'; diff --git a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts index 17f121eb29ab3..9406e1caaed2a 100644 --- a/packages/@aws-cdk/aws-iot/lib/topic-rule.ts +++ b/packages/@aws-cdk/aws-iot/lib/topic-rule.ts @@ -1,5 +1,6 @@ -import { ArnFormat, Resource, Stack, IResource } from '@aws-cdk/core'; +import { ArnFormat, Resource, Stack, IResource, Lazy } from '@aws-cdk/core'; import { Construct } from 'constructs'; +import { IAction } from './action'; import { IotSql } from './iot-sql'; import { CfnTopicRule } from './iot.generated'; @@ -33,6 +34,13 @@ export interface TopicRuleProps { */ readonly topicRuleName?: string; + /** + * The actions associated with the rule. + * + * @default No actions will be perform + */ + readonly actions?: Array; + /** * A simplified SQL syntax to filter messages received on an MQTT topic and push the data elsewhere. * @@ -80,6 +88,8 @@ export class TopicRule extends Resource implements ITopicRule { */ public readonly topicRuleName: string; + private readonly actions = new Array(); + constructor(scope: Construct, id: string, props: TopicRuleProps) { super(scope, id, { physicalName: props.topicRuleName, @@ -90,7 +100,7 @@ export class TopicRule extends Resource implements ITopicRule { const resource = new CfnTopicRule(this, 'Resource', { ruleName: this.physicalName, topicRulePayload: { - actions: [], + actions: Lazy.any({ produce: () => this.actions }), awsIotSqlVersion: sqlConfig.awsIotSqlVersion, sql: sqlConfig.sql, }, @@ -102,5 +112,28 @@ export class TopicRule extends Resource implements ITopicRule { resourceName: this.physicalName, }); this.topicRuleName = this.getResourceNameAttribute(resource.ref); + + props.actions?.forEach(action => { + this.addAction(action); + }); + } + + /** + * Add a action to the rule. + * + * @param action the action to associate with the rule. + */ + public addAction(action: IAction): void { + const { configuration } = action.bind(this); + + const keys = Object.keys(configuration); + if (keys.length === 0) { + throw new Error('An action property cannot be an empty object.'); + } + if (keys.length >= 2) { + throw new Error(`An action property cannot have multiple keys, received: ${keys}`); + } + + this.actions.push(configuration); } } diff --git a/packages/@aws-cdk/aws-iot/test/integ.topic-rule.expected.json b/packages/@aws-cdk/aws-iot/test/integ.topic-rule.expected.json index cf4be2735229e..9daad98410825 100644 --- a/packages/@aws-cdk/aws-iot/test/integ.topic-rule.expected.json +++ b/packages/@aws-cdk/aws-iot/test/integ.topic-rule.expected.json @@ -4,7 +4,13 @@ "Type": "AWS::IoT::TopicRule", "Properties": { "TopicRulePayload": { - "Actions": [], + "Actions": [ + { + "Http": { + "Url": "https://example.com" + } + } + ], "AwsIotSqlVersion": "2015-10-08", "Sql": "SELECT topic(2) as device_id FROM 'device/+/data'" } diff --git a/packages/@aws-cdk/aws-iot/test/integ.topic-rule.ts b/packages/@aws-cdk/aws-iot/test/integ.topic-rule.ts index a06edc3c3f5e1..0f4bab54a9d2a 100644 --- a/packages/@aws-cdk/aws-iot/test/integ.topic-rule.ts +++ b/packages/@aws-cdk/aws-iot/test/integ.topic-rule.ts @@ -10,6 +10,15 @@ class TestStack extends cdk.Stack { new iot.TopicRule(this, 'TopicRule', { sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id FROM 'device/+/data'"), + actions: [ + { + bind: () => ({ + configuration: { + http: { url: 'https://example.com' }, + }, + }), + }, + ], }); } } diff --git a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts index 1dec8c3065a86..d9e8990c26c75 100644 --- a/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts +++ b/packages/@aws-cdk/aws-iot/test/topic-rule.test.ts @@ -100,6 +100,119 @@ test.each([ }).toThrow('IoT SQL string cannot be empty'); }); +test('can set actions', () => { + const stack = new cdk.Stack(); + + const action1: iot.IAction = { + bind: () => ({ + configuration: { + http: { url: 'http://example.com' }, + }, + }), + }; + const action2: iot.IAction = { + bind: () => ({ + configuration: { + lambda: { functionArn: 'test-functionArn' }, + }, + }), + }; + + new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"), + actions: [action1, action2], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Http: { Url: 'http://example.com' }, + }, + { + Lambda: { FunctionArn: 'test-functionArn' }, + }, + ], + Sql: "SELECT topic(2) as device_id, temperature FROM 'device/+/data'", + }, + }); +}); + +test('can add an action', () => { + const stack = new cdk.Stack(); + + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"), + }); + topicRule.addAction({ + bind: () => ({ + configuration: { + http: { url: 'http://example.com' }, + }, + }), + }); + topicRule.addAction({ + bind: () => ({ + configuration: { + lambda: { functionArn: 'test-functionArn' }, + }, + }), + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IoT::TopicRule', { + TopicRulePayload: { + Actions: [ + { + Http: { Url: 'http://example.com' }, + }, + { + Lambda: { FunctionArn: 'test-functionArn' }, + }, + ], + Sql: "SELECT topic(2) as device_id, temperature FROM 'device/+/data'", + }, + }); +}); + +test('cannot add an action as empty object', () => { + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"), + }); + + const emptyKeysAction: iot.IAction = { + bind: () => ({ + configuration: {}, + }), + }; + + expect(() => { + topicRule.addAction(emptyKeysAction); + }).toThrow('An action property cannot be an empty object.'); +}); + +test('cannot add an action that have multiple keys', () => { + const stack = new cdk.Stack(); + const topicRule = new iot.TopicRule(stack, 'MyTopicRule', { + sql: iot.IotSql.fromStringAsVer20151008("SELECT topic(2) as device_id, temperature FROM 'device/+/data'"), + }); + + const multipleKeysAction: iot.IAction = { + bind: () => ({ + configuration: { + http: { url: 'http://example.com' }, + lambda: { functionArn: 'test-functionArn' }, + }, + }), + }; + + expect(() => { + topicRule.addAction(multipleKeysAction); + }).toThrow( + 'An action property cannot have multiple keys, received: http,lambda', + ); +}); + test('can import a TopicRule by ARN', () => { const stack = new cdk.Stack();