From 61a4bae4456c73c323160e6f880d958f5855193d Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Tue, 8 Feb 2022 22:55:47 +0900 Subject: [PATCH 01/29] feat(iotevents): support actions --- packages/@aws-cdk/aws-iotevents/README.md | 196 +++++++++++++++--- packages/@aws-cdk/aws-iotevents/lib/action.ts | 24 +++ .../aws-iotevents/lib/detector-model.ts | 2 +- packages/@aws-cdk/aws-iotevents/lib/event.ts | 8 + packages/@aws-cdk/aws-iotevents/lib/index.ts | 1 + packages/@aws-cdk/aws-iotevents/lib/state.ts | 94 ++++++--- .../aws-iotevents/test/detector-model.test.ts | 27 +++ .../test/integ.detector-model.expected.json | 116 ++++++++++- .../test/integ.detector-model.ts | 60 ++++-- 9 files changed, 455 insertions(+), 73 deletions(-) create mode 100644 packages/@aws-cdk/aws-iotevents/lib/action.ts diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index 809bac071ef7d..7c641f9025833 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -40,65 +40,211 @@ Import it into your code: import * as iotevents from '@aws-cdk/aws-iotevents'; ``` -## `DetectorModel` +## Overview -The following example creates an AWS IoT Events detector model to your stack. -The detector model need a reference to at least one AWS IoT Events input. -AWS IoT Events inputs enable the detector to get MQTT payload values from IoT Core rules. +The following example is a minimal set of an AWS IoT Events detector model. +It has no feature but it maybe help you to understand overview. ```ts import * as iotevents from '@aws-cdk/aws-iotevents'; +// First, define the input of the detector model const input = new iotevents.Input(this, 'MyInput', { - inputName: 'my_input', // optional attributeJsonPaths: ['payload.deviceId', 'payload.temperature'], }); -const warmState = new iotevents.State({ +// Second, define states of the detector model. +// You can define multiple states and its transitions. +const state = new iotevents.State({ stateName: 'warm', onEnter: [{ - eventName: 'test-event', + eventName: 'onEnter', condition: iotevents.Expression.currentInput(input), }], }); -const coldState = new iotevents.State({ - stateName: 'cold', + +// Finally, define the detector model. +new iotevents.DetectorModel(this, 'MyDetectorModel', { + initialState: state, }); +``` -// transit to coldState when temperature is 10 -warmState.transitionTo(coldState, { +Each part is explained in detail below. + +## `Input` + +You can create `Input` as following. You can put messages to the Input with AWS IoT Core Topic Rule, AWS IoT Analytics and more. +For more information, see [the documentation](https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-getting-started.html). + +```ts +import * as iotevents from '@aws-cdk/aws-iotevents'; + +const input = new iotevents.Input(this, 'MyInput', { + inputName: 'my_input', // optional + attributeJsonPaths: ['payload.deviceId', 'payload.temperature'], +}); +``` + +To grant permissions to put messages in the input, +you can use the `grantWrite()` method: + +```ts +import * as iam from '@aws-cdk/aws-iam'; +import * as iotevents from '@aws-cdk/aws-iotevents'; + +declare const grantable: iam.IGrantable; +const input = iotevents.Input.fromInputName(this, 'MyInput', 'my_input'); + +input.grantWrite(grantable); +``` + +## `State` + +You can create `State` as following. +In `onEnter` of a detector model's initial state, at least one reference of input via `condition` is needed. +And if the `condition` is evaluated to `TRUE`, the detector instance are created. +You can set the reference of input with `iotevents.Expression.currentInput()` or `iotevents.Expression.inputAttribute()` as following. +In other states, `onEnter` is optional. + +```ts +import * as iotevents from '@aws-cdk/aws-iotevents'; + +declare const input: iotevents.IInput; + +const initialState = new iotevents.State({ + stateName: 'MyState', + onEnter: [{ + eventName: 'onEnter', + condition: iotevents.Expression.currentInput(input), + }], +}); +``` + +You can set actions to the `onEnter` event. It is caused if `condition` is evaluated to `TRUE`. +If you omit `condition`, actions is caused on every enter events of the state. +For more information, see [supported actions](https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-supported-actions.html). + +```ts +import * as iotevents from '@aws-cdk/aws-iotevents'; + +declare const input: iotevents.IInput; + +const setTemperatureAction = { + bind: () => ({ + configuration: { + setVariable: { variableName: 'temperature', value: temperatureAttr.evaluate() }, + }, + }), +}; + +const state = new iotevents.State({ + stateName: 'MyState', + onEnter: [{ // optional + eventName: 'onEnter', + actions: [setTemperatureAction], // optional + condition: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('10'), + ), // optional + }], +}); +``` + +Also you can use `onInput` and `onExit`. `onInput` is triggered when messages are put to the input +that is refered from the detector model. `onExit` is triggered when exiting this state. + +```ts +import * as iotevents from '@aws-cdk/aws-iotevents'; + +const state = new iotevents.State({ + stateName: 'warm', + onEnter: [{ // optional + eventName: 'onEnter', + }], + onInput: [{ // optional + eventName: 'onInput', + }], + onExit: [{ // optional + eventName: 'onExit', + }], +}); +``` + +You can set transitions of the states as following: + +```ts +import * as iotevents from '@aws-cdk/aws-iotevents'; + +declare const input: iotevents.IInput; +declare const action: iotevents.IAction; +declare const stateA: iotevents.State; +declare const stateB: iotevents.State; + +// transit from stateA to stateB when temperature is 10 +stateA.transitionTo(stateB, { eventName: 'to_coldState', // optional property, default by combining the names of the States + actions: [action], // optional, when: iotevents.Expression.eq( iotevents.Expression.inputAttribute(input, 'payload.temperature'), iotevents.Expression.fromString('10'), ), }); -// transit to warmState when temperature is 20 -coldState.transitionTo(warmState, { - when: iotevents.Expression.eq( - iotevents.Expression.inputAttribute(input, 'payload.temperature'), - iotevents.Expression.fromString('20'), - ), -}); +``` + +## `DetectorModel` + +You can create `DetectorModel` as following. + +```ts +import * as iotevents from '@aws-cdk/aws-iotevents'; + +declare const state: iotevents.State; new iotevents.DetectorModel(this, 'MyDetectorModel', { detectorModelName: 'test-detector-model', // optional description: 'test-detector-model-description', // optional property, default is none evaluationMethod: iotevents.EventEvaluation.SERIAL, // optional property, default is iotevents.EventEvaluation.BATCH detectorKey: 'payload.deviceId', // optional property, default is none and single detector instance will be created and all inputs will be routed to it - initialState: warmState, + initialState: state, }); ``` -To grant permissions to put messages in the input, -you can use the `grantWrite()` method: +## Examples + +The following example creates an AWS IoT Events detector model to your stack. +The State of this detector model transits according to the temperature. ```ts -import * as iam from '@aws-cdk/aws-iam'; import * as iotevents from '@aws-cdk/aws-iotevents'; -declare const grantable: iam.IGrantable; -const input = iotevents.Input.fromInputName(this, 'MyInput', 'my_input'); +const input = new iotevents.Input(this, 'MyInput', { + attributeJsonPaths: ['payload.deviceId', 'payload.temperature'], +}); -input.grantWrite(grantable); +const warmState = new iotevents.State({ + stateName: 'warm', + onEnter: [{ + eventName: 'onEnter', + condition: iotevents.Expression.currentInput(input), + }], +}); +const coldState = new iotevents.State({ + stateName: 'cold', +}); + +const temperatureEqual = (temperature: string) => + iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('10'), + ) + +// transit to coldState when temperature is 10 +warmState.transitionTo(coldState, { when: temperatureEqual('10') }); +// transit to warmState when temperature is 20 +coldState.transitionTo(warmState, { when: temperatureEqual('20') }); + +new iotevents.DetectorModel(this, 'MyDetectorModel', { + detectorKey: 'payload.deviceId', + initialState: warmState, +}); ``` diff --git a/packages/@aws-cdk/aws-iotevents/lib/action.ts b/packages/@aws-cdk/aws-iotevents/lib/action.ts new file mode 100644 index 0000000000000..e48bfa784cee2 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents/lib/action.ts @@ -0,0 +1,24 @@ +import { IDetectorModel } from './detector-model'; +import { CfnDetectorModel } from './iotevents.generated'; + +/** + * An abstract action for DetectorModel. + */ +export interface IAction { + /** + * Returns the detector model action specification. + * + * @param detectorModel The DetectorModel that would trigger this action. + */ + bind(detectorModel: IDetectorModel): ActionConfig; +} + +/** + * Properties for a detector model action + */ +export interface ActionConfig { + /** + * The configuration for this action. + */ + readonly configuration: CfnDetectorModel.ActionProperty; +} diff --git a/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts b/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts index 35128bc4531e6..535bca3bafdbe 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts @@ -124,7 +124,7 @@ export class DetectorModel extends Resource implements IDetectorModel { key: props.detectorKey, detectorModelDefinition: { initialStateName: props.initialState.stateName, - states: props.initialState._collectStateJsons(new Set()), + states: props.initialState.bind(this), }, roleArn: role.roleArn, }); diff --git a/packages/@aws-cdk/aws-iotevents/lib/event.ts b/packages/@aws-cdk/aws-iotevents/lib/event.ts index 610469db9c32c..98d145dc2c110 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/event.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/event.ts @@ -1,3 +1,4 @@ +import { IAction } from './action'; import { Expression } from './expression'; /** @@ -15,4 +16,11 @@ export interface Event { * @default - none (the actions are always executed) */ readonly condition?: Expression; + + /** + * The actions to be performed. + * + * @default - none + */ + readonly actions?: IAction[]; } diff --git a/packages/@aws-cdk/aws-iotevents/lib/index.ts b/packages/@aws-cdk/aws-iotevents/lib/index.ts index 24913635ebe50..b949a47454c3a 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/index.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/index.ts @@ -1,3 +1,4 @@ +export * from './action'; export * from './detector-model'; export * from './event'; export * from './expression'; diff --git a/packages/@aws-cdk/aws-iotevents/lib/state.ts b/packages/@aws-cdk/aws-iotevents/lib/state.ts index 67ee6a32802ec..001e2e0ea1e10 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/state.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -1,3 +1,5 @@ +import { IAction } from './action'; +import { IDetectorModel } from './detector-model'; import { Event } from './event'; import { Expression } from './expression'; import { CfnDetectorModel } from './iotevents.generated'; @@ -18,6 +20,13 @@ export interface TransitionOptions { * When this was evaluated to TRUE, the state transition and the actions are triggered. */ readonly when: Expression; + + /** + * The actions to be performed. + * + * @default - none + */ + readonly actions?: IAction[]; } /** @@ -34,6 +43,13 @@ interface TransitionEvent { */ readonly condition: Expression; + /** + * The actions to be performed. + * + * @default - none + */ + readonly actions?: IAction[]; + /** * The next state to transit to. When the resuld of condition expression is TRUE, the state is transited. */ @@ -50,12 +66,28 @@ export interface StateProps { readonly stateName: string; /** - * Specifies the events on enter. the conditions of the events are evaluated when the state is entered. + * Specifies the events on enter. the conditions of the events are evaluated when entering this state. * If the condition is `TRUE`, the actions of the event are performed. * * @default - events on enter will not be set */ readonly onEnter?: Event[]; + + /** + * Specifies the events on inputted. the conditions of the events are evaluated when an input is received. + * If the condition is `TRUE`, the actions of the event are performed. + * + * @default - events on inputted will not be set + */ + readonly onInput?: Event[]; + + /** + * Specifies the events on exit. the conditions of the events are evaluated when exiting this state. + * If the condition is `TRUE`, the actions of the event are performed. + * + * @default - events on exit will not be set + */ + readonly onExit?: Event[]; } /** @@ -90,28 +122,15 @@ export class State { eventName: options.eventName ?? `${this.stateName}_to_${targetState.stateName}`, nextState: targetState, condition: options.when, + actions: options.actions, }); } /** - * Collect states in dependency gragh that constructed by state transitions, - * and return the JSONs of the states. - * This function is called recursively and collect the states. - * - * @internal + * Return the JSONs of the states in dependency gragh that constructed by state transitions */ - public _collectStateJsons(collectedStates: Set): CfnDetectorModel.StateProperty[] { - if (collectedStates.has(this)) { - return []; - } - collectedStates.add(this); - - return [ - this.toStateJson(), - ...this.transitionEvents.flatMap(transitionEvent => { - return transitionEvent.nextState._collectStateJsons(collectedStates); - }), - ]; + public bind(detectorModel: IDetectorModel): CfnDetectorModel.StateProperty[] { + return this.collectStates(new Set()).map(state => state.toStateJson(detectorModel)); } /** @@ -123,26 +142,52 @@ export class State { return this.props.onEnter?.some(event => event.condition) ?? false; } - private toStateJson(): CfnDetectorModel.StateProperty { - const { onEnter } = this.props; + private collectStates(collectedStates: Set): State[] { + if (collectedStates.has(this)) { + return []; + } + collectedStates.add(this); + + return [this, ...this.transitionEvents.flatMap(transitionEvent => transitionEvent.nextState.collectStates(collectedStates))]; + } + + private toStateJson(detectorModel: IDetectorModel): CfnDetectorModel.StateProperty { + const { onEnter, onInput, onExit } = this.props; return { stateName: this.stateName, - onEnter: onEnter && { events: toEventsJson(onEnter) }, + onEnter: { + events: toEventsJson(detectorModel, onEnter), + }, onInput: { - transitionEvents: toTransitionEventsJson(this.transitionEvents), + events: toEventsJson(detectorModel, onInput), + transitionEvents: toTransitionEventsJson(detectorModel, this.transitionEvents), + }, + onExit: { + events: toEventsJson(detectorModel, onExit), }, }; } } -function toEventsJson(events: Event[]): CfnDetectorModel.EventProperty[] { +function toEventsJson( + detectorModel: IDetectorModel, + events?: Event[], +): CfnDetectorModel.EventProperty[] | undefined { + if (!events) { + return undefined; + } + return events.map(event => ({ eventName: event.eventName, condition: event.condition?.evaluate(), + actions: event.actions?.map(action => action.bind(detectorModel).configuration), })); } -function toTransitionEventsJson(transitionEvents: TransitionEvent[]): CfnDetectorModel.TransitionEventProperty[] | undefined { +function toTransitionEventsJson( + detectorModel: IDetectorModel, + transitionEvents: TransitionEvent[], +): CfnDetectorModel.TransitionEventProperty[] | undefined { if (transitionEvents.length === 0) { return undefined; } @@ -150,6 +195,7 @@ function toTransitionEventsJson(transitionEvents: TransitionEvent[]): CfnDetecto return transitionEvents.map(transitionEvent => ({ eventName: transitionEvent.eventName, condition: transitionEvent.condition.evaluate(), + actions: transitionEvent.actions?.map(action => action.bind(detectorModel).configuration), nextState: transitionEvent.nextState.stateName, })); } diff --git a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts index c90a10cf34374..c781e1a15c09b 100644 --- a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts +++ b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts @@ -109,6 +109,7 @@ test('can set multiple events to State', () => { { eventName: 'test-eventName1', condition: iotevents.Expression.fromString('test-eventCondition'), + actions: [{ bind: () => ({ configuration: { setTimer: { timerName: 'test-timer' } } }) }], }, { eventName: 'test-eventName2', @@ -127,6 +128,7 @@ test('can set multiple events to State', () => { { EventName: 'test-eventName1', Condition: 'test-eventCondition', + Actions: [{ SetTimer: { TimerName: 'test-timer' } }], }, { EventName: 'test-eventName2', @@ -139,6 +141,29 @@ test('can set multiple events to State', () => { }); }); +test.each([ + ['onInput', { onInput: [{ eventName: 'test-eventName1' }] }, { OnInput: { Events: [{ EventName: 'test-eventName1' }] } }], + ['onExit', { onExit: [{ eventName: 'test-eventName1' }] }, { OnExit: { Events: [{ EventName: 'test-eventName1' }] } }], +])('can set %s to State', (_, events, expected) => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ eventName: 'test-eventName1', condition: iotevents.Expression.currentInput(input) }], + ...events, + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [ + Match.objectLike(expected), + ], + }, + }); +}); + test('can set states with transitions', () => { // GIVEN const firstState = new iotevents.State({ @@ -162,6 +187,7 @@ test('can set states with transitions', () => { iotevents.Expression.inputAttribute(input, 'payload.temperature'), iotevents.Expression.fromString('12'), ), + actions: [{ bind: () => ({ configuration: { setTimer: { timerName: 'test-timer' } } }) }], }); // transition as 2nd -> 1st, make circular reference secondState.transitionTo(firstState, { @@ -194,6 +220,7 @@ test('can set states with transitions', () => { EventName: 'firstState_to_secondState', NextState: 'secondState', Condition: '$input.test-input.payload.temperature == 12', + Actions: [{ SetTimer: { TimerName: 'test-timer' } }], }], }, }, diff --git a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json index 888869a41e68e..5556a3cee387f 100644 --- a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json +++ b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json @@ -43,6 +43,25 @@ "OnEnter": { "Events": [ { + "Actions": [ + { + "SetVariable": { + "Value": { + "Fn::Join": [ + "", + [ + "$input.", + { + "Ref": "MyInput08947B23" + }, + ".payload.temperature" + ] + ] + }, + "VariableName": "temperature" + } + } + ], "Condition": { "Fn::Join": [ "", @@ -59,13 +78,106 @@ ] ] }, - "EventName": "test-event" + "EventName": "test-enter-event" + } + ] + }, + "OnExit": { + "Events": [ + { + "Actions": [ + { + "SetVariable": { + "Value": { + "Fn::Join": [ + "", + [ + "$input.", + { + "Ref": "MyInput08947B23" + }, + ".payload.temperature" + ] + ] + }, + "VariableName": "temperature" + } + } + ], + "Condition": { + "Fn::Join": [ + "", + [ + "$input.", + { + "Ref": "MyInput08947B23" + }, + ".payload.temperature == 31.7" + ] + ] + }, + "EventName": "test-exit-event" } ] }, "OnInput": { + "Events": [ + { + "Actions": [ + { + "SetVariable": { + "Value": { + "Fn::Join": [ + "", + [ + "$input.", + { + "Ref": "MyInput08947B23" + }, + ".payload.temperature" + ] + ] + }, + "VariableName": "temperature" + } + } + ], + "Condition": { + "Fn::Join": [ + "", + [ + "$input.", + { + "Ref": "MyInput08947B23" + }, + ".payload.temperature == 31.6" + ] + ] + }, + "EventName": "test-input-event" + } + ], "TransitionEvents": [ { + "Actions": [ + { + "SetVariable": { + "Value": { + "Fn::Join": [ + "", + [ + "$input.", + { + "Ref": "MyInput08947B23" + }, + ".payload.temperature" + ] + ] + }, + "VariableName": "temperature" + } + } + ], "Condition": { "Fn::Join": [ "", @@ -86,6 +198,8 @@ "StateName": "online" }, { + "OnEnter": {}, + "OnExit": {}, "OnInput": { "TransitionEvents": [ { diff --git a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts index 5f6d2839f3a93..62305f55801ba 100644 --- a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts +++ b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts @@ -1,3 +1,11 @@ +/** + * Stack verification steps: + * * put a message + * * aws iotevents-data batch-put-message --messages=messageId=(date | md5),inputName=test_input,payload=(echo '{"payload":{"temperature":31.9,"deviceId":"000"}}' | base64) + * * describe the detector + * * aws iotevents-data describe-detector --detector-model-name test-detector-model --key-value=000 + * * verify `stateName` and `variables` of the detector + */ import * as cdk from '@aws-cdk/core'; import * as iotevents from '../lib'; @@ -10,38 +18,46 @@ class TestStack extends cdk.Stack { attributeJsonPaths: ['payload.deviceId', 'payload.temperature'], }); + const inputted = iotevents.Expression.currentInput(input); + const temperatureAttr = iotevents.Expression.inputAttribute(input, 'payload.temperature'); + const temperatureEqual = (temperature: string) => iotevents.Expression.eq( + temperatureAttr, + iotevents.Expression.fromString(temperature), + ); + + const setTemperatureAction = { + bind: () => ({ + configuration: { + setVariable: { variableName: 'temperature', value: temperatureAttr.evaluate() }, + }, + }), + }; + const onlineState = new iotevents.State({ stateName: 'online', onEnter: [{ - eventName: 'test-event', + eventName: 'test-enter-event', // meaning `condition: 'currentInput("test_input") && $input.test_input.payload.temperature == 31.5'` - condition: iotevents.Expression.and( - iotevents.Expression.currentInput(input), - iotevents.Expression.eq( - iotevents.Expression.inputAttribute(input, 'payload.temperature'), - iotevents.Expression.fromString('31.5'), - ), - ), + condition: iotevents.Expression.and(inputted, temperatureEqual('31.5')), + actions: [setTemperatureAction], + }], + onInput: [{ + eventName: 'test-input-event', + condition: temperatureEqual('31.6'), + actions: [setTemperatureAction], + }], + onExit: [{ + eventName: 'test-exit-event', + condition: temperatureEqual('31.7'), + actions: [setTemperatureAction], }], }); const offlineState = new iotevents.State({ stateName: 'offline', }); - // 1st => 2nd - onlineState.transitionTo(offlineState, { - when: iotevents.Expression.eq( - iotevents.Expression.inputAttribute(input, 'payload.temperature'), - iotevents.Expression.fromString('12'), - ), - }); - // 2st => 1st - offlineState.transitionTo(onlineState, { - when: iotevents.Expression.eq( - iotevents.Expression.inputAttribute(input, 'payload.temperature'), - iotevents.Expression.fromString('21'), - ), - }); + onlineState.transitionTo(offlineState, { when: temperatureEqual('12'), actions: [setTemperatureAction] }); + offlineState.transitionTo(onlineState, { when: temperatureEqual('21') }); new iotevents.DetectorModel(this, 'MyDetectorModel', { detectorModelName: 'test-detector-model', From f3bdd56c6c8d90a568824855f4bc960fcc6f569e Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Tue, 8 Feb 2022 23:52:21 +0900 Subject: [PATCH 02/29] fix code comments --- packages/@aws-cdk/aws-iotevents/README.md | 16 ++++++++-------- packages/@aws-cdk/aws-iotevents/lib/action.ts | 6 +++--- .../aws-iotevents/test/detector-model.test.ts | 4 +--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index 7c641f9025833..a01caac5d5e24 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -102,7 +102,7 @@ input.grantWrite(grantable); You can create `State` as following. In `onEnter` of a detector model's initial state, at least one reference of input via `condition` is needed. -And if the `condition` is evaluated to `TRUE`, the detector instance are created. +And if messages is put to the input, the detector instance are created regardless of the evaluation result of the `condition`. You can set the reference of input with `iotevents.Expression.currentInput()` or `iotevents.Expression.inputAttribute()` as following. In other states, `onEnter` is optional. @@ -120,8 +120,8 @@ const initialState = new iotevents.State({ }); ``` -You can set actions to the `onEnter` event. It is caused if `condition` is evaluated to `TRUE`. -If you omit `condition`, actions is caused on every enter events of the state. +You can set actions to the `onEnter` event. It is performed if `condition` is evaluated to `TRUE`. +If you omit `condition`, actions is performed on every enter events of the state. For more information, see [supported actions](https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-supported-actions.html). ```ts @@ -132,7 +132,10 @@ declare const input: iotevents.IInput; const setTemperatureAction = { bind: () => ({ configuration: { - setVariable: { variableName: 'temperature', value: temperatureAttr.evaluate() }, + setVariable: { + variableName: 'temperature', + value: iotevents.Expression.inputAttribute(input, 'payload.temperature').evaluate(), + }, }, }), }; @@ -142,10 +145,7 @@ const state = new iotevents.State({ onEnter: [{ // optional eventName: 'onEnter', actions: [setTemperatureAction], // optional - condition: iotevents.Expression.eq( - iotevents.Expression.inputAttribute(input, 'payload.temperature'), - iotevents.Expression.fromString('10'), - ), // optional + condition: iotevents.Expression.currentInput(input), // optional }], }); ``` diff --git a/packages/@aws-cdk/aws-iotevents/lib/action.ts b/packages/@aws-cdk/aws-iotevents/lib/action.ts index e48bfa784cee2..f60a3babc62e1 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/action.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/action.ts @@ -6,15 +6,15 @@ import { CfnDetectorModel } from './iotevents.generated'; */ export interface IAction { /** - * Returns the detector model action specification. + * Returns the AWS IoT Events action specification. * - * @param detectorModel The DetectorModel that would trigger this action. + * @param detectorModel The DetectorModel that has the state this action is set in. */ bind(detectorModel: IDetectorModel): ActionConfig; } /** - * Properties for a detector model action + * Properties for a AWS IoT Events action */ export interface ActionConfig { /** diff --git a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts index c781e1a15c09b..348368a9e8f2f 100644 --- a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts +++ b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts @@ -157,9 +157,7 @@ test.each([ // THEN Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { DetectorModelDefinition: { - States: [ - Match.objectLike(expected), - ], + States: [Match.objectLike(expected)], }, }); }); From 940e1c7e87b3ddd465aeebbb15dfa78b51ddfc07 Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Tue, 8 Feb 2022 23:53:45 +0900 Subject: [PATCH 03/29] remove redundant comments --- packages/@aws-cdk/aws-iotevents/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index a01caac5d5e24..401c92662a1ab 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -238,9 +238,7 @@ const temperatureEqual = (temperature: string) => iotevents.Expression.fromString('10'), ) -// transit to coldState when temperature is 10 warmState.transitionTo(coldState, { when: temperatureEqual('10') }); -// transit to warmState when temperature is 20 coldState.transitionTo(warmState, { when: temperatureEqual('20') }); new iotevents.DetectorModel(this, 'MyDetectorModel', { From 5b86fa8412ff2e2186567470b3cffa747fd79bf4 Mon Sep 17 00:00:00 2001 From: Tatsuya Yamamoto Date: Wed, 9 Feb 2022 23:15:25 +0900 Subject: [PATCH 04/29] Update packages/@aws-cdk/aws-iotevents/README.md Co-authored-by: Adam Ruka --- packages/@aws-cdk/aws-iotevents/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index 401c92662a1ab..a5f1980c432f1 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -101,9 +101,11 @@ input.grantWrite(grantable); ## `State` You can create `State` as following. -In `onEnter` of a detector model's initial state, at least one reference of input via `condition` is needed. -And if messages is put to the input, the detector instance are created regardless of the evaluation result of the `condition`. -You can set the reference of input with `iotevents.Expression.currentInput()` or `iotevents.Expression.inputAttribute()` as following. +If a State is used for a Detector Model's initial state, +it's required that its `onEnter` Event is non-null, +and contains a reference to an Input via the `condition` property. +And if a message is put to the input, the detector instances are created regardless of the evaluation result of `condition`. +You can set the reference to input with `iotevents.Expression.currentInput()` or `iotevents.Expression.inputAttribute()`. In other states, `onEnter` is optional. ```ts From 7ad05323a637c6db510d2f3efde02bbd0139e8d5 Mon Sep 17 00:00:00 2001 From: Tatsuya Yamamoto Date: Wed, 9 Feb 2022 23:16:52 +0900 Subject: [PATCH 05/29] Update packages/@aws-cdk/aws-iotevents/README.md Co-authored-by: Adam Ruka --- packages/@aws-cdk/aws-iotevents/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index a5f1980c432f1..51fc5d264e86b 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -122,7 +122,7 @@ const initialState = new iotevents.State({ }); ``` -You can set actions to the `onEnter` event. It is performed if `condition` is evaluated to `TRUE`. +You can set actions on the `onEnter` event. They are performed if `condition` evaluates to `true`. If you omit `condition`, actions is performed on every enter events of the state. For more information, see [supported actions](https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-supported-actions.html). From a021c79bdc716e1858bc0dc75c62caaceb14cb6f Mon Sep 17 00:00:00 2001 From: Tatsuya Yamamoto Date: Wed, 9 Feb 2022 23:17:44 +0900 Subject: [PATCH 06/29] Update packages/@aws-cdk/aws-iotevents/README.md Co-authored-by: Adam Ruka --- packages/@aws-cdk/aws-iotevents/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index 51fc5d264e86b..ee1552c89f6cc 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -123,7 +123,7 @@ const initialState = new iotevents.State({ ``` You can set actions on the `onEnter` event. They are performed if `condition` evaluates to `true`. -If you omit `condition`, actions is performed on every enter events of the state. +If you omit `condition`, actions are performed every time the state is entered. For more information, see [supported actions](https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-supported-actions.html). ```ts From 814e352dc0a1d6cdf226b81229746e5d3908b3ac Mon Sep 17 00:00:00 2001 From: Tatsuya Yamamoto Date: Wed, 9 Feb 2022 23:31:00 +0900 Subject: [PATCH 07/29] Update packages/@aws-cdk/aws-iotevents/README.md Co-authored-by: Adam Ruka --- packages/@aws-cdk/aws-iotevents/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index ee1552c89f6cc..10732c22a3bee 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -152,8 +152,9 @@ const state = new iotevents.State({ }); ``` -Also you can use `onInput` and `onExit`. `onInput` is triggered when messages are put to the input -that is refered from the detector model. `onExit` is triggered when exiting this state. +You can also use the `onInput` and `onExit` properties. +`onInput` is triggered when messages are put to the input that is referenced from the detector model. +`onExit` is triggered when exiting this state. ```ts import * as iotevents from '@aws-cdk/aws-iotevents'; From 2e9bca1689e4e348fc805d52aa26ba1a2499bce8 Mon Sep 17 00:00:00 2001 From: Tatsuya Yamamoto Date: Wed, 9 Feb 2022 23:32:25 +0900 Subject: [PATCH 08/29] Update packages/@aws-cdk/aws-iotevents/README.md Co-authored-by: Adam Ruka --- packages/@aws-cdk/aws-iotevents/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index 10732c22a3bee..5b63874d2b0fa 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -196,7 +196,7 @@ stateA.transitionTo(stateB, { ## `DetectorModel` -You can create `DetectorModel` as following. +You can create a Detector Model as follows: ```ts import * as iotevents from '@aws-cdk/aws-iotevents'; From a430030e31a8d0abf432881511990e35824828a4 Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Thu, 10 Feb 2022 00:16:33 +0900 Subject: [PATCH 09/29] address comments about README --- packages/@aws-cdk/aws-iotevents/README.md | 56 +++++------------------ 1 file changed, 12 insertions(+), 44 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index 5b63874d2b0fa..94b1638773587 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -40,38 +40,7 @@ Import it into your code: import * as iotevents from '@aws-cdk/aws-iotevents'; ``` -## Overview - -The following example is a minimal set of an AWS IoT Events detector model. -It has no feature but it maybe help you to understand overview. - -```ts -import * as iotevents from '@aws-cdk/aws-iotevents'; - -// First, define the input of the detector model -const input = new iotevents.Input(this, 'MyInput', { - attributeJsonPaths: ['payload.deviceId', 'payload.temperature'], -}); - -// Second, define states of the detector model. -// You can define multiple states and its transitions. -const state = new iotevents.State({ - stateName: 'warm', - onEnter: [{ - eventName: 'onEnter', - condition: iotevents.Expression.currentInput(input), - }], -}); - -// Finally, define the detector model. -new iotevents.DetectorModel(this, 'MyDetectorModel', { - initialState: state, -}); -``` - -Each part is explained in detail below. - -## `Input` +## Input You can create `Input` as following. You can put messages to the Input with AWS IoT Core Topic Rule, AWS IoT Analytics and more. For more information, see [the documentation](https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-getting-started.html). @@ -85,7 +54,7 @@ const input = new iotevents.Input(this, 'MyInput', { }); ``` -To grant permissions to put messages in the input, +To grant permissions to put messages in the Input, you can use the `grantWrite()` method: ```ts @@ -93,19 +62,18 @@ import * as iam from '@aws-cdk/aws-iam'; import * as iotevents from '@aws-cdk/aws-iotevents'; declare const grantable: iam.IGrantable; -const input = iotevents.Input.fromInputName(this, 'MyInput', 'my_input'); +declare const input: iotevents.IInput; input.grantWrite(grantable); ``` -## `State` +## State You can create `State` as following. -If a State is used for a Detector Model's initial state, -it's required that its `onEnter` Event is non-null, +If a State is used for a Detector Model's initial State, it's required that its `onEnter` Event is non-null, and contains a reference to an Input via the `condition` property. -And if a message is put to the input, the detector instances are created regardless of the evaluation result of `condition`. -You can set the reference to input with `iotevents.Expression.currentInput()` or `iotevents.Expression.inputAttribute()`. +And if a message is put to the Input, the detector instances are created regardless of the evaluation result of `condition`. +You can set the reference to Input with `iotevents.Expression.currentInput()` or `iotevents.Expression.inputAttribute()`. In other states, `onEnter` is optional. ```ts @@ -123,7 +91,7 @@ const initialState = new iotevents.State({ ``` You can set actions on the `onEnter` event. They are performed if `condition` evaluates to `true`. -If you omit `condition`, actions are performed every time the state is entered. +If you omit `condition`, actions are performed every time the State is entered. For more information, see [supported actions](https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-supported-actions.html). ```ts @@ -153,8 +121,8 @@ const state = new iotevents.State({ ``` You can also use the `onInput` and `onExit` properties. -`onInput` is triggered when messages are put to the input that is referenced from the detector model. -`onExit` is triggered when exiting this state. +`onInput` is triggered when messages are put to the Input that is referenced from the detector model. +`onExit` is triggered when exiting this State. ```ts import * as iotevents from '@aws-cdk/aws-iotevents'; @@ -186,15 +154,15 @@ declare const stateB: iotevents.State; // transit from stateA to stateB when temperature is 10 stateA.transitionTo(stateB, { eventName: 'to_coldState', // optional property, default by combining the names of the States - actions: [action], // optional, when: iotevents.Expression.eq( iotevents.Expression.inputAttribute(input, 'payload.temperature'), iotevents.Expression.fromString('10'), ), + actions: [action], // optional, }); ``` -## `DetectorModel` +## DetectorModel You can create a Detector Model as follows: From 31d35164388ca81d5fb4194ef54430f2966f3d10 Mon Sep 17 00:00:00 2001 From: Tatsuya Yamamoto Date: Thu, 10 Feb 2022 00:19:41 +0900 Subject: [PATCH 10/29] Update packages/@aws-cdk/aws-iotevents/lib/event.ts Co-authored-by: Adam Ruka --- packages/@aws-cdk/aws-iotevents/lib/event.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-iotevents/lib/event.ts b/packages/@aws-cdk/aws-iotevents/lib/event.ts index 98d145dc2c110..6bdf0490b39b7 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/event.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/event.ts @@ -20,7 +20,7 @@ export interface Event { /** * The actions to be performed. * - * @default - none + * @default - no actions will be performed */ readonly actions?: IAction[]; } From 7bd8f97cbe8148ca35689ab6a896c4119c8fe4a4 Mon Sep 17 00:00:00 2001 From: Tatsuya Yamamoto Date: Thu, 10 Feb 2022 00:20:46 +0900 Subject: [PATCH 11/29] Update packages/@aws-cdk/aws-iotevents/lib/state.ts Co-authored-by: Adam Ruka --- packages/@aws-cdk/aws-iotevents/lib/state.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/lib/state.ts b/packages/@aws-cdk/aws-iotevents/lib/state.ts index 001e2e0ea1e10..ebdbd7879bfa4 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/state.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -66,8 +66,8 @@ export interface StateProps { readonly stateName: string; /** - * Specifies the events on enter. the conditions of the events are evaluated when entering this state. - * If the condition is `TRUE`, the actions of the event are performed. + * Specifies the events on enter. The conditions of the events will be evaluated when entering this state. + * If the condition of the event evaluates to `true`, the actions of the event will be executed. * * @default - events on enter will not be set */ From ebfeeb28d6dcdb1c2f26fa93010e33f415e4e607 Mon Sep 17 00:00:00 2001 From: Tatsuya Yamamoto Date: Thu, 10 Feb 2022 00:21:17 +0900 Subject: [PATCH 12/29] Update packages/@aws-cdk/aws-iotevents/lib/state.ts Co-authored-by: Adam Ruka --- packages/@aws-cdk/aws-iotevents/lib/state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-iotevents/lib/state.ts b/packages/@aws-cdk/aws-iotevents/lib/state.ts index ebdbd7879bfa4..747182692fcd9 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/state.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -69,7 +69,7 @@ export interface StateProps { * Specifies the events on enter. The conditions of the events will be evaluated when entering this state. * If the condition of the event evaluates to `true`, the actions of the event will be executed. * - * @default - events on enter will not be set + * @default - no events will trigger on entering this state */ readonly onEnter?: Event[]; From a12366607ea8acd4c1d22d63f691dfd0fbd48578 Mon Sep 17 00:00:00 2001 From: Tatsuya Yamamoto Date: Thu, 10 Feb 2022 00:22:25 +0900 Subject: [PATCH 13/29] Update packages/@aws-cdk/aws-iotevents/lib/state.ts Co-authored-by: Adam Ruka --- packages/@aws-cdk/aws-iotevents/lib/state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-iotevents/lib/state.ts b/packages/@aws-cdk/aws-iotevents/lib/state.ts index 747182692fcd9..1a33e8eee90e5 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/state.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -77,7 +77,7 @@ export interface StateProps { * Specifies the events on inputted. the conditions of the events are evaluated when an input is received. * If the condition is `TRUE`, the actions of the event are performed. * - * @default - events on inputted will not be set + * @default - no events will trigger on input in this state */ readonly onInput?: Event[]; From 20c47d20ccb2d73627fed9332f326834a1748284 Mon Sep 17 00:00:00 2001 From: Tatsuya Yamamoto Date: Thu, 10 Feb 2022 00:24:03 +0900 Subject: [PATCH 14/29] Update packages/@aws-cdk/aws-iotevents/lib/state.ts Co-authored-by: Adam Ruka --- packages/@aws-cdk/aws-iotevents/lib/state.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/lib/state.ts b/packages/@aws-cdk/aws-iotevents/lib/state.ts index 1a33e8eee90e5..847083790f883 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/state.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -74,8 +74,8 @@ export interface StateProps { readonly onEnter?: Event[]; /** - * Specifies the events on inputted. the conditions of the events are evaluated when an input is received. - * If the condition is `TRUE`, the actions of the event are performed. + * Specifies the events on input. The conditions of the events will be evaluated when any input is received. + * If the condition of the event evaluates to `true`, the actions of the event will be executed. * * @default - no events will trigger on input in this state */ From 9e67025369c5a66d25a4265aa84eb06dca917760 Mon Sep 17 00:00:00 2001 From: Tatsuya Yamamoto Date: Thu, 10 Feb 2022 00:24:17 +0900 Subject: [PATCH 15/29] Update packages/@aws-cdk/aws-iotevents/lib/state.ts Co-authored-by: Adam Ruka --- packages/@aws-cdk/aws-iotevents/lib/state.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/lib/state.ts b/packages/@aws-cdk/aws-iotevents/lib/state.ts index 847083790f883..4393fe89c8a20 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/state.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -82,8 +82,8 @@ export interface StateProps { readonly onInput?: Event[]; /** - * Specifies the events on exit. the conditions of the events are evaluated when exiting this state. - * If the condition is `TRUE`, the actions of the event are performed. + * Specifies the events on exit. The conditions of the events are evaluated when an exiting this state. + * If the condition evaluates to `true`, the actions of the event will be executed. * * @default - events on exit will not be set */ From 9a62326c7eb0a5fbf4c6f4b7c1166d2d18efaa76 Mon Sep 17 00:00:00 2001 From: Tatsuya Yamamoto Date: Thu, 10 Feb 2022 00:24:31 +0900 Subject: [PATCH 16/29] Update packages/@aws-cdk/aws-iotevents/lib/state.ts Co-authored-by: Adam Ruka --- packages/@aws-cdk/aws-iotevents/lib/state.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-iotevents/lib/state.ts b/packages/@aws-cdk/aws-iotevents/lib/state.ts index 4393fe89c8a20..7c3e8ea789317 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/state.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -85,7 +85,7 @@ export interface StateProps { * Specifies the events on exit. The conditions of the events are evaluated when an exiting this state. * If the condition evaluates to `true`, the actions of the event will be executed. * - * @default - events on exit will not be set + * @default - no events will trigger on exiting this state */ readonly onExit?: Event[]; } From 9cc9794140dd1acb8c7fd08d85a35a3a2092e8af Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Thu, 10 Feb 2022 23:55:58 +0900 Subject: [PATCH 17/29] address comments --- packages/@aws-cdk/aws-iotevents/lib/action.ts | 5 +- .../aws-iotevents/lib/detector-model.ts | 2 +- packages/@aws-cdk/aws-iotevents/lib/event.ts | 4 +- packages/@aws-cdk/aws-iotevents/lib/state.ts | 74 +++++++++---------- .../aws-iotevents/test/detector-model.test.ts | 4 +- .../test/integ.detector-model.ts | 6 +- 6 files changed, 45 insertions(+), 50 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/lib/action.ts b/packages/@aws-cdk/aws-iotevents/lib/action.ts index f60a3babc62e1..4d54a25cca42b 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/action.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/action.ts @@ -1,4 +1,3 @@ -import { IDetectorModel } from './detector-model'; import { CfnDetectorModel } from './iotevents.generated'; /** @@ -7,10 +6,8 @@ import { CfnDetectorModel } from './iotevents.generated'; export interface IAction { /** * Returns the AWS IoT Events action specification. - * - * @param detectorModel The DetectorModel that has the state this action is set in. */ - bind(detectorModel: IDetectorModel): ActionConfig; + renderActionConfig(): ActionConfig; } /** diff --git a/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts b/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts index 535bca3bafdbe..35128bc4531e6 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts @@ -124,7 +124,7 @@ export class DetectorModel extends Resource implements IDetectorModel { key: props.detectorKey, detectorModelDefinition: { initialStateName: props.initialState.stateName, - states: props.initialState.bind(this), + states: props.initialState._collectStateJsons(new Set()), }, roleArn: role.roleArn, }); diff --git a/packages/@aws-cdk/aws-iotevents/lib/event.ts b/packages/@aws-cdk/aws-iotevents/lib/event.ts index 6bdf0490b39b7..fd452686e054e 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/event.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/event.ts @@ -2,7 +2,7 @@ import { IAction } from './action'; import { Expression } from './expression'; /** - * Specifies the actions to be performed when the condition evaluates to TRUE. + * Specifies the actions to be performed when the condition evaluates to `true`. */ export interface Event { /** @@ -11,7 +11,7 @@ export interface Event { readonly eventName: string; /** - * The Boolean expression that, when TRUE, causes the actions to be performed. + * The Boolean expression that, when `true`, causes the actions to be performed. * * @default - none (the actions are always executed) */ diff --git a/packages/@aws-cdk/aws-iotevents/lib/state.ts b/packages/@aws-cdk/aws-iotevents/lib/state.ts index 7c3e8ea789317..4c814a7348fdd 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/state.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -1,5 +1,4 @@ import { IAction } from './action'; -import { IDetectorModel } from './detector-model'; import { Event } from './event'; import { Expression } from './expression'; import { CfnDetectorModel } from './iotevents.generated'; @@ -17,20 +16,20 @@ export interface TransitionOptions { /** * The condition that is used to determine to cause the state transition and the actions. - * When this was evaluated to TRUE, the state transition and the actions are triggered. + * When this was evaluated to `true`, the state transition and the actions are triggered. */ readonly when: Expression; /** - * The actions to be performed. + * The actions to be performed with the transition. * - * @default - none + * @default - no actions will be performed */ - readonly actions?: IAction[]; + readonly executing?: IAction[]; } /** - * Specifies the state transition and the actions to be performed when the condition evaluates to TRUE. + * Specifies the state transition and the actions to be performed when the condition evaluates to `true`. */ interface TransitionEvent { /** @@ -39,19 +38,19 @@ interface TransitionEvent { readonly eventName: string; /** - * The Boolean expression that, when TRUE, causes the state transition and the actions to be performed. + * The Boolean expression that, when `true`, causes the state transition and the actions to be performed. */ readonly condition: Expression; /** * The actions to be performed. * - * @default - none + * @default - no actions will be performed */ readonly actions?: IAction[]; /** - * The next state to transit to. When the resuld of condition expression is TRUE, the state is transited. + * The next state to transit to. When the resuld of condition expression is `true`, the state is transited. */ readonly nextState: State; } @@ -107,7 +106,7 @@ export class State { /** * Add a transition event to the state. - * The transition event will be triggered if condition is evaluated to TRUE. + * The transition event will be triggered if condition is evaluated to `true`. * * @param targetState the state that will be transit to when the event triggered * @param options transition options including the condition that causes the state transition @@ -122,15 +121,29 @@ export class State { eventName: options.eventName ?? `${this.stateName}_to_${targetState.stateName}`, nextState: targetState, condition: options.when, - actions: options.actions, + actions: options.executing, }); } /** - * Return the JSONs of the states in dependency gragh that constructed by state transitions + * Collect states in dependency gragh that constructed by state transitions, + * and return the JSONs of the states. + * This function is called recursively and collect the states. + * + * @internal */ - public bind(detectorModel: IDetectorModel): CfnDetectorModel.StateProperty[] { - return this.collectStates(new Set()).map(state => state.toStateJson(detectorModel)); + public _collectStateJsons(collectedStates: Set): CfnDetectorModel.StateProperty[] { + if (collectedStates.has(this)) { + return []; + } + collectedStates.add(this); + + return [ + this.toStateJson(), + ...this.transitionEvents.flatMap(transitionEvent => { + return transitionEvent.nextState._collectStateJsons(collectedStates); + }), + ]; } /** @@ -142,37 +155,25 @@ export class State { return this.props.onEnter?.some(event => event.condition) ?? false; } - private collectStates(collectedStates: Set): State[] { - if (collectedStates.has(this)) { - return []; - } - collectedStates.add(this); - - return [this, ...this.transitionEvents.flatMap(transitionEvent => transitionEvent.nextState.collectStates(collectedStates))]; - } - - private toStateJson(detectorModel: IDetectorModel): CfnDetectorModel.StateProperty { + private toStateJson(): CfnDetectorModel.StateProperty { const { onEnter, onInput, onExit } = this.props; return { stateName: this.stateName, onEnter: { - events: toEventsJson(detectorModel, onEnter), + events: toEventsJson(onEnter), }, onInput: { - events: toEventsJson(detectorModel, onInput), - transitionEvents: toTransitionEventsJson(detectorModel, this.transitionEvents), + events: toEventsJson(onInput), + transitionEvents: toTransitionEventsJson(this.transitionEvents), }, onExit: { - events: toEventsJson(detectorModel, onExit), + events: toEventsJson(onExit), }, }; } } -function toEventsJson( - detectorModel: IDetectorModel, - events?: Event[], -): CfnDetectorModel.EventProperty[] | undefined { +function toEventsJson(events?: Event[]): CfnDetectorModel.EventProperty[] | undefined { if (!events) { return undefined; } @@ -180,14 +181,11 @@ function toEventsJson( return events.map(event => ({ eventName: event.eventName, condition: event.condition?.evaluate(), - actions: event.actions?.map(action => action.bind(detectorModel).configuration), + actions: event.actions?.map(action => action.renderActionConfig().configuration), })); } -function toTransitionEventsJson( - detectorModel: IDetectorModel, - transitionEvents: TransitionEvent[], -): CfnDetectorModel.TransitionEventProperty[] | undefined { +function toTransitionEventsJson(transitionEvents: TransitionEvent[]): CfnDetectorModel.TransitionEventProperty[] | undefined { if (transitionEvents.length === 0) { return undefined; } @@ -195,7 +193,7 @@ function toTransitionEventsJson( return transitionEvents.map(transitionEvent => ({ eventName: transitionEvent.eventName, condition: transitionEvent.condition.evaluate(), - actions: transitionEvent.actions?.map(action => action.bind(detectorModel).configuration), + actions: transitionEvent.actions?.map(action => action.renderActionConfig().configuration), nextState: transitionEvent.nextState.stateName, })); } diff --git a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts index 348368a9e8f2f..c0948a0e1b5fe 100644 --- a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts +++ b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts @@ -109,7 +109,7 @@ test('can set multiple events to State', () => { { eventName: 'test-eventName1', condition: iotevents.Expression.fromString('test-eventCondition'), - actions: [{ bind: () => ({ configuration: { setTimer: { timerName: 'test-timer' } } }) }], + actions: [{ renderActionConfig: () => ({ configuration: { setTimer: { timerName: 'test-timer' } } }) }], }, { eventName: 'test-eventName2', @@ -185,7 +185,7 @@ test('can set states with transitions', () => { iotevents.Expression.inputAttribute(input, 'payload.temperature'), iotevents.Expression.fromString('12'), ), - actions: [{ bind: () => ({ configuration: { setTimer: { timerName: 'test-timer' } } }) }], + executing: [{ renderActionConfig: () => ({ configuration: { setTimer: { timerName: 'test-timer' } } }) }], }); // transition as 2nd -> 1st, make circular reference secondState.transitionTo(firstState, { diff --git a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts index 62305f55801ba..4097cf7d1b579 100644 --- a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts +++ b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts @@ -25,8 +25,8 @@ class TestStack extends cdk.Stack { iotevents.Expression.fromString(temperature), ); - const setTemperatureAction = { - bind: () => ({ + const setTemperatureAction: iotevents.IAction = { + renderActionConfig: () => ({ configuration: { setVariable: { variableName: 'temperature', value: temperatureAttr.evaluate() }, }, @@ -56,7 +56,7 @@ class TestStack extends cdk.Stack { stateName: 'offline', }); - onlineState.transitionTo(offlineState, { when: temperatureEqual('12'), actions: [setTemperatureAction] }); + onlineState.transitionTo(offlineState, { when: temperatureEqual('12'), executing: [setTemperatureAction] }); offlineState.transitionTo(onlineState, { when: temperatureEqual('21') }); new iotevents.DetectorModel(this, 'MyDetectorModel', { From 218b91e77ac7b5b651a4bbb6e1bda07ab9017430 Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Fri, 11 Feb 2022 00:21:27 +0900 Subject: [PATCH 18/29] address comments about tests --- .../aws-iotevents/test/detector-model.test.ts | 74 ++++++++++++++++++- .../test/integ.detector-model.expected.json | 2 +- .../test/integ.detector-model.ts | 47 ++++++++---- 3 files changed, 104 insertions(+), 19 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts index c0948a0e1b5fe..1ea0270f971be 100644 --- a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts +++ b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts @@ -109,7 +109,6 @@ test('can set multiple events to State', () => { { eventName: 'test-eventName1', condition: iotevents.Expression.fromString('test-eventCondition'), - actions: [{ renderActionConfig: () => ({ configuration: { setTimer: { timerName: 'test-timer' } } }) }], }, { eventName: 'test-eventName2', @@ -128,7 +127,6 @@ test('can set multiple events to State', () => { { EventName: 'test-eventName1', Condition: 'test-eventCondition', - Actions: [{ SetTimer: { TimerName: 'test-timer' } }], }, { EventName: 'test-eventName2', @@ -141,6 +139,35 @@ test('can set multiple events to State', () => { }); }); +test('can set actions to events', () => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName1', + condition: iotevents.Expression.currentInput(input), + actions: [{ renderActionConfig: () => ({ configuration: { setTimer: { timerName: 'test-timer' } } }) }], + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [ + Match.objectLike({ + OnEnter: { + Events: [{ + Actions: [{ SetTimer: { TimerName: 'test-timer' } }], + }], + }, + }), + ], + }, + }); +}); + test.each([ ['onInput', { onInput: [{ eventName: 'test-eventName1' }] }, { OnInput: { Events: [{ EventName: 'test-eventName1' }] } }], ['onExit', { onExit: [{ eventName: 'test-eventName1' }] }, { OnExit: { Events: [{ EventName: 'test-eventName1' }] } }], @@ -185,7 +212,6 @@ test('can set states with transitions', () => { iotevents.Expression.inputAttribute(input, 'payload.temperature'), iotevents.Expression.fromString('12'), ), - executing: [{ renderActionConfig: () => ({ configuration: { setTimer: { timerName: 'test-timer' } } }) }], }); // transition as 2nd -> 1st, make circular reference secondState.transitionTo(firstState, { @@ -218,7 +244,6 @@ test('can set states with transitions', () => { EventName: 'firstState_to_secondState', NextState: 'secondState', Condition: '$input.test-input.payload.temperature == 12', - Actions: [{ SetTimer: { TimerName: 'test-timer' } }], }], }, }, @@ -247,6 +272,47 @@ test('can set states with transitions', () => { }); }); +test('can set actions to transitions', () => { + // GIVEN + const firstState = new iotevents.State({ + stateName: 'firstState', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.currentInput(input), + }], + }); + const secondState = new iotevents.State({ + stateName: 'secondState', + }); + + // WHEN + firstState.transitionTo(secondState, { + when: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('12'), + ), + executing: [{ renderActionConfig: () => ({ configuration: { setTimer: { timerName: 'test-timer' } } }) }], + }); + + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: firstState, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: Match.arrayWith([Match.objectLike({ + StateName: 'firstState', + OnInput: { + TransitionEvents: [{ + Actions: [{ SetTimer: { TimerName: 'test-timer' } }], + }], + }, + })]), + }, + }); +}); + test('can set role', () => { // WHEN const role = iam.Role.fromRoleArn(stack, 'test-role', 'arn:aws:iam::123456789012:role/ForTest'); diff --git a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json index 5556a3cee387f..c04885e5480df 100644 --- a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json +++ b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json @@ -78,7 +78,7 @@ ] ] }, - "EventName": "test-enter-event" + "EventName": "test-event" } ] }, diff --git a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts index 4097cf7d1b579..95af14ddeaea8 100644 --- a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts +++ b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts @@ -18,17 +18,13 @@ class TestStack extends cdk.Stack { attributeJsonPaths: ['payload.deviceId', 'payload.temperature'], }); - const inputted = iotevents.Expression.currentInput(input); - const temperatureAttr = iotevents.Expression.inputAttribute(input, 'payload.temperature'); - const temperatureEqual = (temperature: string) => iotevents.Expression.eq( - temperatureAttr, - iotevents.Expression.fromString(temperature), - ); - const setTemperatureAction: iotevents.IAction = { renderActionConfig: () => ({ configuration: { - setVariable: { variableName: 'temperature', value: temperatureAttr.evaluate() }, + setVariable: { + variableName: 'temperature', + value: iotevents.Expression.inputAttribute(input, 'payload.temperature').evaluate(), + }, }, }), }; @@ -36,19 +32,31 @@ class TestStack extends cdk.Stack { const onlineState = new iotevents.State({ stateName: 'online', onEnter: [{ - eventName: 'test-enter-event', + eventName: 'test-event', // meaning `condition: 'currentInput("test_input") && $input.test_input.payload.temperature == 31.5'` - condition: iotevents.Expression.and(inputted, temperatureEqual('31.5')), + condition: iotevents.Expression.and( + iotevents.Expression.currentInput(input), + iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('31.5'), + ), + ), actions: [setTemperatureAction], }], onInput: [{ eventName: 'test-input-event', - condition: temperatureEqual('31.6'), + condition: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('31.6'), + ), actions: [setTemperatureAction], }], onExit: [{ eventName: 'test-exit-event', - condition: temperatureEqual('31.7'), + condition: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('31.7'), + ), actions: [setTemperatureAction], }], }); @@ -56,8 +64,19 @@ class TestStack extends cdk.Stack { stateName: 'offline', }); - onlineState.transitionTo(offlineState, { when: temperatureEqual('12'), executing: [setTemperatureAction] }); - offlineState.transitionTo(onlineState, { when: temperatureEqual('21') }); + onlineState.transitionTo(offlineState, { + when: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('12'), + ), + executing: [setTemperatureAction], + }); + offlineState.transitionTo(onlineState, { + when: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('21'), + ), + }); new iotevents.DetectorModel(this, 'MyDetectorModel', { detectorModelName: 'test-detector-model', From c7e142d556b071348433471396d63b8db584e2c7 Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Sat, 12 Feb 2022 22:41:58 +0900 Subject: [PATCH 19/29] fix readme --- packages/@aws-cdk/aws-iotevents/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index 94b1638773587..c58549922b9cc 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -100,7 +100,7 @@ import * as iotevents from '@aws-cdk/aws-iotevents'; declare const input: iotevents.IInput; const setTemperatureAction = { - bind: () => ({ + renderActionConfig: () => ({ configuration: { setVariable: { variableName: 'temperature', @@ -158,7 +158,7 @@ stateA.transitionTo(stateB, { iotevents.Expression.inputAttribute(input, 'payload.temperature'), iotevents.Expression.fromString('10'), ), - actions: [action], // optional, + executing: [action], // optional, }); ``` From 297e3b15ef18b0dc999217a0ca81f7a6eb149398 Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Mon, 21 Feb 2022 23:27:56 +0900 Subject: [PATCH 20/29] add sns action --- .../aws-iotevents-actions/lib/index.ts | 3 +- .../lib/sns-topic-publish-action.ts | 32 +++++ .../aws-iotevents-actions/package.json | 6 + .../test/iotevents-actions.test.ts | 6 - ...teg.sns-topic-publish-action.expected.json | 116 ++++++++++++++++++ .../sns/integ.sns-topic-publish-action.ts | 46 +++++++ .../test/sns/sns-topic-publish-action.test.ts | 61 +++++++++ packages/@aws-cdk/aws-iotevents/lib/action.ts | 6 + .../aws-iotevents/lib/detector-model.ts | 6 +- packages/@aws-cdk/aws-iotevents/lib/state.ts | 45 +++++-- tools/@aws-cdk/pkglint/lib/rules.ts | 1 + 11 files changed, 307 insertions(+), 21 deletions(-) create mode 100644 packages/@aws-cdk/aws-iotevents-actions/lib/sns-topic-publish-action.ts delete mode 100644 packages/@aws-cdk/aws-iotevents-actions/test/iotevents-actions.test.ts create mode 100644 packages/@aws-cdk/aws-iotevents-actions/test/sns/integ.sns-topic-publish-action.expected.json create mode 100644 packages/@aws-cdk/aws-iotevents-actions/test/sns/integ.sns-topic-publish-action.ts create mode 100644 packages/@aws-cdk/aws-iotevents-actions/test/sns/sns-topic-publish-action.test.ts diff --git a/packages/@aws-cdk/aws-iotevents-actions/lib/index.ts b/packages/@aws-cdk/aws-iotevents-actions/lib/index.ts index 3e4b0ef0a73d4..84863eb44f382 100644 --- a/packages/@aws-cdk/aws-iotevents-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-iotevents-actions/lib/index.ts @@ -1,2 +1 @@ -// this is placeholder for monocdk -export const dummy = true; +export * from './sns-topic-publish-action'; diff --git a/packages/@aws-cdk/aws-iotevents-actions/lib/sns-topic-publish-action.ts b/packages/@aws-cdk/aws-iotevents-actions/lib/sns-topic-publish-action.ts new file mode 100644 index 0000000000000..cdf3567da16dd --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/lib/sns-topic-publish-action.ts @@ -0,0 +1,32 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as iotevents from '@aws-cdk/aws-iotevents'; +import * as sns from '@aws-cdk/aws-sns'; + +/** + * The action to write the data to an Amazon SNS topic. + */ +export class SNSTopicPublishAction implements iotevents.IAction { + readonly actionPolicies?: iam.PolicyStatement[]; + + /** + * @param topic The Amazon SNS topic to publish data on. Must not be a FIFO topic. + */ + constructor(private readonly topic: sns.ITopic) { + this.actionPolicies = [ + new iam.PolicyStatement({ + actions: ['sns:Publish'], + resources: [topic.topicArn], + }), + ]; + } + + renderActionConfig(): iotevents.ActionConfig { + return { + configuration: { + sns: { + targetArn: this.topic.topicArn, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iotevents-actions/package.json b/packages/@aws-cdk/aws-iotevents-actions/package.json index 81cfc5979c269..941451fc3b04a 100644 --- a/packages/@aws-cdk/aws-iotevents-actions/package.json +++ b/packages/@aws-cdk/aws-iotevents-actions/package.json @@ -78,10 +78,16 @@ "jest": "^27.5.1" }, "dependencies": { + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-iotevents": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/core": "0.0.0" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-iotevents": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/core": "0.0.0" }, "engines": { diff --git a/packages/@aws-cdk/aws-iotevents-actions/test/iotevents-actions.test.ts b/packages/@aws-cdk/aws-iotevents-actions/test/iotevents-actions.test.ts deleted file mode 100644 index 465c7bdea0693..0000000000000 --- a/packages/@aws-cdk/aws-iotevents-actions/test/iotevents-actions.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assertions'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/aws-iotevents-actions/test/sns/integ.sns-topic-publish-action.expected.json b/packages/@aws-cdk/aws-iotevents-actions/test/sns/integ.sns-topic-publish-action.expected.json new file mode 100644 index 0000000000000..6aa92e7788561 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/test/sns/integ.sns-topic-publish-action.expected.json @@ -0,0 +1,116 @@ +{ + "Resources": { + "MyInput08947B23": { + "Type": "AWS::IoTEvents::Input", + "Properties": { + "InputDefinition": { + "Attributes": [ + { + "JsonPath": "payload.deviceId" + } + ] + }, + "InputName": "test_input" + } + }, + "MyTopic86869434": { + "Type": "AWS::SNS::Topic" + }, + "MyDetectorModelDetectorModelRoleF2FB4D88": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "iotevents.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyDetectorModelDetectorModelRoleDefaultPolicy82887422": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": { + "Ref": "MyTopic86869434" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyDetectorModelDetectorModelRoleDefaultPolicy82887422", + "Roles": [ + { + "Ref": "MyDetectorModelDetectorModelRoleF2FB4D88" + } + ] + } + }, + "MyDetectorModel559C0B0E": { + "Type": "AWS::IoTEvents::DetectorModel", + "Properties": { + "DetectorModelDefinition": { + "InitialStateName": "MyState", + "States": [ + { + "OnEnter": { + "Events": [ + { + "Condition": { + "Fn::Join": [ + "", + [ + "currentInput(\"", + { + "Ref": "MyInput08947B23" + }, + "\")" + ] + ] + }, + "EventName": "test-event" + } + ] + }, + "OnExit": {}, + "OnInput": { + "Events": [ + { + "Actions": [ + { + "Sns": { + "TargetArn": { + "Ref": "MyTopic86869434" + } + } + } + ], + "EventName": "test-input-event" + } + ] + }, + "StateName": "MyState" + } + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "MyDetectorModelDetectorModelRoleF2FB4D88", + "Arn" + ] + }, + "Key": "payload.deviceId" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iotevents-actions/test/sns/integ.sns-topic-publish-action.ts b/packages/@aws-cdk/aws-iotevents-actions/test/sns/integ.sns-topic-publish-action.ts new file mode 100644 index 0000000000000..5d0bee2578348 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/test/sns/integ.sns-topic-publish-action.ts @@ -0,0 +1,46 @@ +/** + * Stack verification steps: + * * subscribe the topic + * * aws sns subscribe --topic-arn "arn:aws:sns:::" --protocol email --notification-endpoint + * * confirm subscription from email + * * put a message + * * aws iotevents-data batch-put-message --messages=messageId=(date | md5),inputName=test_input,payload=(echo '{"payload":{"temperature":31.9,"deviceId":"000"}}' | base64) + * * verify that an email was sent from the SNS + */ +import * as iotevents from '@aws-cdk/aws-iotevents'; +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +class TestStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const input = new iotevents.Input(this, 'MyInput', { + inputName: 'test_input', + attributeJsonPaths: ['payload.deviceId'], + }); + const topic = new sns.Topic(this, 'MyTopic'); + + const state = new iotevents.State({ + stateName: 'MyState', + onEnter: [{ + eventName: 'test-event', + condition: iotevents.Expression.currentInput(input), + }], + onInput: [{ + eventName: 'test-input-event', + actions: [new actions.SNSTopicPublishAction(topic)], + }], + }); + + new iotevents.DetectorModel(this, 'MyDetectorModel', { + detectorKey: 'payload.deviceId', + initialState: state, + }); + } +} + +const app = new cdk.App(); +new TestStack(app, 'sns-topic-publish-action-test-stack'); +app.synth(); diff --git a/packages/@aws-cdk/aws-iotevents-actions/test/sns/sns-topic-publish-action.test.ts b/packages/@aws-cdk/aws-iotevents-actions/test/sns/sns-topic-publish-action.test.ts new file mode 100644 index 0000000000000..82b2f2a6aa240 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/test/sns/sns-topic-publish-action.test.ts @@ -0,0 +1,61 @@ +import { Template } from '@aws-cdk/assertions'; +import * as iotevents from '@aws-cdk/aws-iotevents'; +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import * as actions from '../../lib'; + +let stack: cdk.Stack; +let input: iotevents.IInput; +let topic: sns.ITopic; +beforeEach(() => { + stack = new cdk.Stack(); + input = iotevents.Input.fromInputName(stack, 'MyInput', 'test-input'); + topic = sns.Topic.fromTopicArn(stack, 'MyTopic', 'arn:aws:sns:us-east-1:1234567890:MyTopic'); +}); + +test('Default property', () => { + // WHEN + new iotevents.DetectorModel(stack, 'MyDetectorModel', { + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName', + condition: iotevents.Expression.currentInput(input), + actions: [new actions.SNSTopicPublishAction(topic)], + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [{ + OnEnter: { + Events: [{ + Actions: [{ + Sns: { + TargetArn: 'arn:aws:sns:us-east-1:1234567890:MyTopic', + }, + }], + }], + }, + }], + }, + RoleArn: { + 'Fn::GetAtt': ['MyDetectorModelDetectorModelRoleF2FB4D88', 'Arn'], + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Action: 'sns:Publish', + Effect: 'Allow', + Resource: 'arn:aws:sns:us-east-1:1234567890:MyTopic', + }], + }, + Roles: [{ + Ref: 'MyDetectorModelDetectorModelRoleF2FB4D88', + }], + }); +}); diff --git a/packages/@aws-cdk/aws-iotevents/lib/action.ts b/packages/@aws-cdk/aws-iotevents/lib/action.ts index 4d54a25cca42b..245f8ba46acec 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/action.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/action.ts @@ -1,9 +1,15 @@ +import * as iam from '@aws-cdk/aws-iam'; import { CfnDetectorModel } from './iotevents.generated'; /** * An abstract action for DetectorModel. */ export interface IAction { + /** + * The policies to perform the AWS IoT Events action. + */ + readonly actionPolicies?: iam.PolicyStatement[]; + /** * Returns the AWS IoT Events action specification. */ diff --git a/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts b/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts index 35128bc4531e6..6f7f8488b2f45 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts @@ -117,6 +117,10 @@ export class DetectorModel extends Resource implements IDetectorModel { assumedBy: new iam.ServicePrincipal('iotevents.amazonaws.com'), }); + props.initialState._collectPolicies().forEach(policy => { + role.addToPrincipalPolicy(policy); + }); + const resource = new CfnDetectorModel(this, 'Resource', { detectorModelName: this.physicalName, detectorModelDescription: props.description, @@ -124,7 +128,7 @@ export class DetectorModel extends Resource implements IDetectorModel { key: props.detectorKey, detectorModelDefinition: { initialStateName: props.initialState.stateName, - states: props.initialState._collectStateJsons(new Set()), + states: props.initialState._collectStateJsons(), }, roleArn: role.roleArn, }); diff --git a/packages/@aws-cdk/aws-iotevents/lib/state.ts b/packages/@aws-cdk/aws-iotevents/lib/state.ts index 4c814a7348fdd..a9b298ee626c1 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/state.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -1,3 +1,4 @@ +import * as iam from '@aws-cdk/aws-iam'; import { IAction } from './action'; import { Event } from './event'; import { Expression } from './expression'; @@ -128,22 +129,28 @@ export class State { /** * Collect states in dependency gragh that constructed by state transitions, * and return the JSONs of the states. - * This function is called recursively and collect the states. * * @internal */ - public _collectStateJsons(collectedStates: Set): CfnDetectorModel.StateProperty[] { - if (collectedStates.has(this)) { - return []; - } - collectedStates.add(this); + public _collectStateJsons(): CfnDetectorModel.StateProperty[] { + return this.collectStates(new Set()).map(state => state.toStateJson()); + } - return [ - this.toStateJson(), - ...this.transitionEvents.flatMap(transitionEvent => { - return transitionEvent.nextState._collectStateJsons(collectedStates); - }), - ]; + /** + * Collect policies to perform the actions in dependency gragh that constructed by state transitions. + * + * @internal + */ + public _collectPolicies(): iam.PolicyStatement[] { + return this.collectStates(new Set()) + .flatMap(state => ([ + ...state.props.onEnter ?? [], + ...state.props.onInput ?? [], + ...state.props.onExit ?? [], + ...state.transitionEvents, + ])) + .flatMap(event => event.actions ?? []) + .flatMap(action => action.actionPolicies ?? []); } /** @@ -155,6 +162,20 @@ export class State { return this.props.onEnter?.some(event => event.condition) ?? false; } + private collectStates(collectedStates: Set): State[] { + if (collectedStates.has(this)) { + return []; + } + collectedStates.add(this); + + return [ + this, + ...this.transitionEvents.flatMap(transitionEvent => { + return transitionEvent.nextState.collectStates(collectedStates); + }), + ]; + } + private toStateJson(): CfnDetectorModel.StateProperty { const { onEnter, onInput, onExit } = this.props; return { diff --git a/tools/@aws-cdk/pkglint/lib/rules.ts b/tools/@aws-cdk/pkglint/lib/rules.ts index 0ff12f6485520..ddc9f65e79d54 100644 --- a/tools/@aws-cdk/pkglint/lib/rules.ts +++ b/tools/@aws-cdk/pkglint/lib/rules.ts @@ -1685,6 +1685,7 @@ export class NoExperimentalDependents extends ValidationRule { ['@aws-cdk/aws-events-targets', ['@aws-cdk/aws-kinesisfirehose']], ['@aws-cdk/aws-kinesisfirehose-destinations', ['@aws-cdk/aws-kinesisfirehose']], ['@aws-cdk/aws-iot-actions', ['@aws-cdk/aws-iot', '@aws-cdk/aws-kinesisfirehose']], + ['@aws-cdk/aws-iotevents-actions', ['@aws-cdk/aws-iotevents']], ]); private readonly excludedModules = ['@aws-cdk/cloudformation-include']; From 8e4452bd39a626462ce85fb0ab8bbe0b5801a728 Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Mon, 28 Feb 2022 22:56:10 +0900 Subject: [PATCH 21/29] lambda actions --- .../aws-iotevents-actions/lib/index.ts | 2 +- .../lib/lambda-invoke-action.ts | 35 ++++++++++ .../lib/sns-topic-publish-action.ts | 32 --------- .../aws-iotevents-actions/package.json | 4 +- .../integ.lambda-invoke-action.expected.json} | 67 +++++++++++++++++-- .../integ.lambda-invoke-action.ts} | 21 +++--- .../lambda-invoke-action.test.ts} | 16 ++--- 7 files changed, 119 insertions(+), 58 deletions(-) create mode 100644 packages/@aws-cdk/aws-iotevents-actions/lib/lambda-invoke-action.ts delete mode 100644 packages/@aws-cdk/aws-iotevents-actions/lib/sns-topic-publish-action.ts rename packages/@aws-cdk/aws-iotevents-actions/test/{sns/integ.sns-topic-publish-action.expected.json => lambda/integ.lambda-invoke-action.expected.json} (61%) rename packages/@aws-cdk/aws-iotevents-actions/test/{sns/integ.sns-topic-publish-action.ts => lambda/integ.lambda-invoke-action.ts} (67%) rename packages/@aws-cdk/aws-iotevents-actions/test/{sns/sns-topic-publish-action.test.ts => lambda/lambda-invoke-action.test.ts} (73%) diff --git a/packages/@aws-cdk/aws-iotevents-actions/lib/index.ts b/packages/@aws-cdk/aws-iotevents-actions/lib/index.ts index 84863eb44f382..4b2ec39329315 100644 --- a/packages/@aws-cdk/aws-iotevents-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-iotevents-actions/lib/index.ts @@ -1 +1 @@ -export * from './sns-topic-publish-action'; +export * from './lambda-invoke-action'; diff --git a/packages/@aws-cdk/aws-iotevents-actions/lib/lambda-invoke-action.ts b/packages/@aws-cdk/aws-iotevents-actions/lib/lambda-invoke-action.ts new file mode 100644 index 0000000000000..4cbc04574622c --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/lib/lambda-invoke-action.ts @@ -0,0 +1,35 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as iotevents from '@aws-cdk/aws-iotevents'; +import * as lambda from '@aws-cdk/aws-lambda'; + +/** + * The action to write the data to an AWS Lambda function. + */ +export class LambdaInvokeAction implements iotevents.IAction { + /** + * The policies to perform the AWS IoT Events action. + */ + readonly actionPolicies?: iam.PolicyStatement[]; + + /** + * @param func the AWS Lambda function to be invoked by this action + */ + constructor(private readonly func: lambda.IFunction) { + this.actionPolicies = [ + new iam.PolicyStatement({ + actions: ['lambda:InvokeFunction'], + resources: [func.functionArn], + }), + ]; + } + + renderActionConfig(): iotevents.ActionConfig { + return { + configuration: { + lambda: { + functionArn: this.func.functionArn, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iotevents-actions/lib/sns-topic-publish-action.ts b/packages/@aws-cdk/aws-iotevents-actions/lib/sns-topic-publish-action.ts deleted file mode 100644 index cdf3567da16dd..0000000000000 --- a/packages/@aws-cdk/aws-iotevents-actions/lib/sns-topic-publish-action.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as iam from '@aws-cdk/aws-iam'; -import * as iotevents from '@aws-cdk/aws-iotevents'; -import * as sns from '@aws-cdk/aws-sns'; - -/** - * The action to write the data to an Amazon SNS topic. - */ -export class SNSTopicPublishAction implements iotevents.IAction { - readonly actionPolicies?: iam.PolicyStatement[]; - - /** - * @param topic The Amazon SNS topic to publish data on. Must not be a FIFO topic. - */ - constructor(private readonly topic: sns.ITopic) { - this.actionPolicies = [ - new iam.PolicyStatement({ - actions: ['sns:Publish'], - resources: [topic.topicArn], - }), - ]; - } - - renderActionConfig(): iotevents.ActionConfig { - return { - configuration: { - sns: { - targetArn: this.topic.topicArn, - }, - }, - }; - } -} diff --git a/packages/@aws-cdk/aws-iotevents-actions/package.json b/packages/@aws-cdk/aws-iotevents-actions/package.json index 941451fc3b04a..24adbf475ed84 100644 --- a/packages/@aws-cdk/aws-iotevents-actions/package.json +++ b/packages/@aws-cdk/aws-iotevents-actions/package.json @@ -80,14 +80,14 @@ "dependencies": { "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-iotevents": "0.0.0", - "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/core": "0.0.0" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-iotevents": "0.0.0", - "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/core": "0.0.0" }, "engines": { diff --git a/packages/@aws-cdk/aws-iotevents-actions/test/sns/integ.sns-topic-publish-action.expected.json b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.expected.json similarity index 61% rename from packages/@aws-cdk/aws-iotevents-actions/test/sns/integ.sns-topic-publish-action.expected.json rename to packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.expected.json index 6aa92e7788561..50eab8e42c8fd 100644 --- a/packages/@aws-cdk/aws-iotevents-actions/test/sns/integ.sns-topic-publish-action.expected.json +++ b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.expected.json @@ -13,8 +13,55 @@ "InputName": "test_input" } }, - "MyTopic86869434": { - "Type": "AWS::SNS::Topic" + "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": "\n exports.handler = (event) => {\n console.log(\"It is test for lambda action of AWS IoT Rule.\", event);\n };" + }, + "Role": { + "Fn::GetAtt": [ + "MyFunctionServiceRole3C357FF2", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "nodejs14.x" + }, + "DependsOn": [ + "MyFunctionServiceRole3C357FF2" + ] }, "MyDetectorModelDetectorModelRoleF2FB4D88": { "Type": "AWS::IAM::Role", @@ -39,10 +86,13 @@ "PolicyDocument": { "Statement": [ { - "Action": "sns:Publish", + "Action": "lambda:InvokeFunction", "Effect": "Allow", "Resource": { - "Ref": "MyTopic86869434" + "Fn::GetAtt": [ + "MyFunction3BAA72D1", + "Arn" + ] } } ], @@ -88,9 +138,12 @@ { "Actions": [ { - "Sns": { - "TargetArn": { - "Ref": "MyTopic86869434" + "Lambda": { + "FunctionArn": { + "Fn::GetAtt": [ + "MyFunction3BAA72D1", + "Arn" + ] } } } diff --git a/packages/@aws-cdk/aws-iotevents-actions/test/sns/integ.sns-topic-publish-action.ts b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.ts similarity index 67% rename from packages/@aws-cdk/aws-iotevents-actions/test/sns/integ.sns-topic-publish-action.ts rename to packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.ts index 5d0bee2578348..9a57f27c22b1a 100644 --- a/packages/@aws-cdk/aws-iotevents-actions/test/sns/integ.sns-topic-publish-action.ts +++ b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.ts @@ -1,14 +1,11 @@ /** * Stack verification steps: - * * subscribe the topic - * * aws sns subscribe --topic-arn "arn:aws:sns:::" --protocol email --notification-endpoint - * * confirm subscription from email * * put a message * * aws iotevents-data batch-put-message --messages=messageId=(date | md5),inputName=test_input,payload=(echo '{"payload":{"temperature":31.9,"deviceId":"000"}}' | base64) - * * verify that an email was sent from the SNS + * * verify that the lambda logs be put */ import * as iotevents from '@aws-cdk/aws-iotevents'; -import * as sns from '@aws-cdk/aws-sns'; +import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import * as actions from '../../lib'; @@ -20,7 +17,15 @@ class TestStack extends cdk.Stack { inputName: 'test_input', attributeJsonPaths: ['payload.deviceId'], }); - const topic = new sns.Topic(this, 'MyTopic'); + const func = 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); + };`, + ), + }); const state = new iotevents.State({ stateName: 'MyState', @@ -30,7 +35,7 @@ class TestStack extends cdk.Stack { }], onInput: [{ eventName: 'test-input-event', - actions: [new actions.SNSTopicPublishAction(topic)], + actions: [new actions.LambdaInvokeAction(func)], }], }); @@ -42,5 +47,5 @@ class TestStack extends cdk.Stack { } const app = new cdk.App(); -new TestStack(app, 'sns-topic-publish-action-test-stack'); +new TestStack(app, 'lambda-invoke-action-test-stack'); app.synth(); diff --git a/packages/@aws-cdk/aws-iotevents-actions/test/sns/sns-topic-publish-action.test.ts b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/lambda-invoke-action.test.ts similarity index 73% rename from packages/@aws-cdk/aws-iotevents-actions/test/sns/sns-topic-publish-action.test.ts rename to packages/@aws-cdk/aws-iotevents-actions/test/lambda/lambda-invoke-action.test.ts index 82b2f2a6aa240..5bf872958d5ef 100644 --- a/packages/@aws-cdk/aws-iotevents-actions/test/sns/sns-topic-publish-action.test.ts +++ b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/lambda-invoke-action.test.ts @@ -1,16 +1,16 @@ import { Template } from '@aws-cdk/assertions'; import * as iotevents from '@aws-cdk/aws-iotevents'; -import * as sns from '@aws-cdk/aws-sns'; +import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; import * as actions from '../../lib'; let stack: cdk.Stack; let input: iotevents.IInput; -let topic: sns.ITopic; +let func: lambda.IFunction; beforeEach(() => { stack = new cdk.Stack(); input = iotevents.Input.fromInputName(stack, 'MyInput', 'test-input'); - topic = sns.Topic.fromTopicArn(stack, 'MyTopic', 'arn:aws:sns:us-east-1:1234567890:MyTopic'); + func = lambda.Function.fromFunctionArn(stack, 'MyFunction', 'arn:aws:lambda:us-east-1:123456789012:function:MyFn'); }); test('Default property', () => { @@ -21,7 +21,7 @@ test('Default property', () => { onEnter: [{ eventName: 'test-eventName', condition: iotevents.Expression.currentInput(input), - actions: [new actions.SNSTopicPublishAction(topic)], + actions: [new actions.LambdaInvokeAction(func)], }], }), }); @@ -33,8 +33,8 @@ test('Default property', () => { OnEnter: { Events: [{ Actions: [{ - Sns: { - TargetArn: 'arn:aws:sns:us-east-1:1234567890:MyTopic', + Lambda: { + FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', }, }], }], @@ -49,9 +49,9 @@ test('Default property', () => { Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [{ - Action: 'sns:Publish', + Action: 'lambda:InvokeFunction', Effect: 'Allow', - Resource: 'arn:aws:sns:us-east-1:1234567890:MyTopic', + Resource: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', }], }, Roles: [{ From 6d80f6941cc26a583eb7bef6872131b738d714ed Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Mon, 28 Feb 2022 23:18:26 +0900 Subject: [PATCH 22/29] write readme --- packages/@aws-cdk/aws-iot-actions/README.md | 264 ++------------------ 1 file changed, 15 insertions(+), 249 deletions(-) diff --git a/packages/@aws-cdk/aws-iot-actions/README.md b/packages/@aws-cdk/aws-iot-actions/README.md index 088fda5f3e5b8..94a5e848ce79f 100644 --- a/packages/@aws-cdk/aws-iot-actions/README.md +++ b/packages/@aws-cdk/aws-iot-actions/README.md @@ -15,266 +15,32 @@ -This library contains integration classes to send data to any number of -supported AWS Services. Instances of these classes should be passed to -`TopicRule` defined in `@aws-cdk/aws-iot`. +This library contains integration classes to use a timer or set a variable, or send data to other AWS resources. +AWS IoT Events can trigger actions when it detects a specified event or transition event. Currently supported are: -- Republish a message to another MQTT topic - Invoke a Lambda function -- Put objects to a S3 bucket -- Put logs to CloudWatch Logs -- Capture CloudWatch metrics -- Change state for a CloudWatch alarm -- Put records to Kinesis Data stream -- Put records to Kinesis Data Firehose stream -- Send messages to SQS queues -- Publish messages on SNS topics - -## Republish a message to another MQTT topic - -The code snippet below creates an AWS IoT Rule that republish a message to -another MQTT topic when it is triggered. - -```ts -new iot.TopicRule(this, 'TopicRule', { - sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'"), - actions: [ - new actions.IotRepublishMqttAction('${topic()}/republish', { - qualityOfService: actions.MqttQualityOfService.AT_LEAST_ONCE, // optional property, default is MqttQualityOfService.ZERO_OR_MORE_TIMES - }), - ], -}); -``` ## Invoke a Lambda function -The code snippet below creates an AWS IoT Rule that invoke a Lambda function -when it is triggered. - -```ts -const func = 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); - };` - ), -}); - -new iot.TopicRule(this, 'TopicRule', { - sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'"), - actions: [new actions.LambdaFunctionAction(func)], -}); -``` - -## Put objects to a S3 bucket - -The code snippet below creates an AWS IoT Rule that put objects to a S3 bucket -when it is triggered. - -```ts -const bucket = new s3.Bucket(this, 'MyBucket'); - -new iot.TopicRule(this, 'TopicRule', { - sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), - actions: [new actions.S3PutObjectAction(bucket)], -}); -``` - -The property `key` of `S3PutObjectAction` is given the value `${topic()}/${timestamp()}` by default. This `${topic()}` -and `${timestamp()}` is called Substitution templates. For more information see -[this documentation](https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html). -In above sample, `${topic()}` is replaced by a given MQTT topic as `device/001/data`. And `${timestamp()}` is replaced -by the number of the current timestamp in milliseconds as `1636289461203`. So if the MQTT broker receives an MQTT topic -`device/001/data` on `2021-11-07T00:00:00.000Z`, the S3 bucket object will be put to `device/001/data/1636243200000`. - -You can also set specific `key` as following: - -```ts -const bucket = new s3.Bucket(this, 'MyBucket'); - -new iot.TopicRule(this, 'TopicRule', { - sql: iot.IotSql.fromStringAsVer20160323( - "SELECT topic(2) as device_id, year, month, day FROM 'device/+/data'", - ), - actions: [ - new actions.S3PutObjectAction(bucket, { - key: '${year}/${month}/${day}/${topic(2)}', - }), - ], -}); -``` - -If you wanna set access control to the S3 bucket object, you can specify `accessControl` as following: - -```ts -const bucket = new s3.Bucket(this, 'MyBucket'); - -new iot.TopicRule(this, 'TopicRule', { - sql: iot.IotSql.fromStringAsVer20160323("SELECT * FROM 'device/+/data'"), - actions: [ - new actions.S3PutObjectAction(bucket, { - accessControl: s3.BucketAccessControl.PUBLIC_READ, - }), - ], -}); -``` - -## Put logs to CloudWatch Logs - -The code snippet below creates an AWS IoT Rule that put logs to CloudWatch Logs -when it is triggered. - -```ts -import * as logs from '@aws-cdk/aws-logs'; - -const logGroup = new logs.LogGroup(this, 'MyLogGroup'); - -new iot.TopicRule(this, 'TopicRule', { - sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), - actions: [new actions.CloudWatchLogsAction(logGroup)], -}); -``` - -## Capture CloudWatch metrics - -The code snippet below creates an AWS IoT Rule that capture CloudWatch metrics +The code snippet below creates an Action that invoke a Lambda function when it is triggered. ```ts -const topicRule = new iot.TopicRule(this, 'TopicRule', { - sql: iot.IotSql.fromStringAsVer20160323( - "SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'", - ), - actions: [ - new actions.CloudWatchPutMetricAction({ - metricName: '${topic(2)}', - metricNamespace: '${namespace}', - metricUnit: '${unit}', - metricValue: '${value}', - metricTimestamp: '${timestamp}', - }), - ], -}); -``` - -## Change the state of an Amazon CloudWatch alarm - -The code snippet below creates an AWS IoT Rule that changes the state of an Amazon CloudWatch alarm when it is triggered: - -```ts -import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; - -const metric = new cloudwatch.Metric({ - namespace: 'MyNamespace', - metricName: 'MyMetric', - dimensions: { MyDimension: 'MyDimensionValue' }, -}); -const alarm = new cloudwatch.Alarm(this, 'MyAlarm', { - metric: metric, - threshold: 100, - evaluationPeriods: 3, - datapointsToAlarm: 2, -}); - -const topicRule = new iot.TopicRule(this, 'TopicRule', { - sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), - actions: [ - new actions.CloudWatchSetAlarmStateAction(alarm, { - reason: 'AWS Iot Rule action is triggered', - alarmStateToSet: cloudwatch.AlarmState.ALARM, - }), - ], -}); -``` - -## Put records to Kinesis Data stream - -The code snippet below creates an AWS IoT Rule that put records to Kinesis Data -stream when it is triggered. - -```ts -import * as kinesis from '@aws-cdk/aws-kinesis'; - -const stream = new kinesis.Stream(this, 'MyStream'); - -const topicRule = new iot.TopicRule(this, 'TopicRule', { - sql: iot.IotSql.fromStringAsVer20160323("SELECT * FROM 'device/+/data'"), - actions: [ - new actions.KinesisPutRecordAction(stream, { - partitionKey: '${newuuid()}', - }), - ], -}); -``` - -## Put records to Kinesis Data Firehose stream - -The code snippet below creates an AWS IoT Rule that put records to Put records -to Kinesis Data Firehose stream when it is triggered. - -```ts -import * as firehose from '@aws-cdk/aws-kinesisfirehose'; -import * as destinations from '@aws-cdk/aws-kinesisfirehose-destinations'; - -const bucket = new s3.Bucket(this, 'MyBucket'); -const stream = new firehose.DeliveryStream(this, 'MyStream', { - destinations: [new destinations.S3Bucket(bucket)], -}); - -const topicRule = new iot.TopicRule(this, 'TopicRule', { - sql: iot.IotSql.fromStringAsVer20160323("SELECT * FROM 'device/+/data'"), - actions: [ - new actions.FirehosePutRecordAction(stream, { - batchMode: true, - recordSeparator: actions.FirehoseRecordSeparator.NEWLINE, - }), - ], -}); -``` - -## Send messages to an SQS queue - -The code snippet below creates an AWS IoT Rule that send messages -to an SQS queue when it is triggered: - -```ts -import * as sqs from '@aws-cdk/aws-sqs'; - -const queue = new sqs.Queue(this, 'MyQueue'); - -const topicRule = new iot.TopicRule(this, 'TopicRule', { - sql: iot.IotSql.fromStringAsVer20160323( - "SELECT topic(2) as device_id, year, month, day FROM 'device/+/data'", - ), - actions: [ - new actions.SqsQueueAction(queue, { - useBase64: true, // optional property, default is 'false' - }), - ], -}); -``` - -## Publish messages on an SNS topic - -The code snippet below creates and AWS IoT Rule that publishes messages to an SNS topic when it is triggered: - -```ts -import * as sns from '@aws-cdk/aws-sns'; +import * as iotevents from '@aws-cdk/aws-iotevents'; +import * as actions from '@aws-cdk/aws-iotevents-actions'; +import * as lambda from '@aws-cdk/aws-lambda'; -const topic = new sns.Topic(this, 'MyTopic'); +declare const input: iotevents.IInput; +declare const func: lambda.IFunction; -const topicRule = new iot.TopicRule(this, 'TopicRule', { - sql: iot.IotSql.fromStringAsVer20160323( - "SELECT topic(2) as device_id, year, month, day FROM 'device/+/data'", - ), - actions: [ - new actions.SnsTopicAction(topic, { - messageFormat: actions.SnsActionMessageFormat.JSON, // optional property, default is SnsActionMessageFormat.RAW - }), - ], +const state = new iotevents.State({ + stateName: 'MyState', + onEnter: [{ + eventName: 'test-event', + condition: iotevents.Expression.currentInput(input), + actions: [new actions.LambdaInvokeAction(func)], + }], }); ``` From 011f1eb9cc3429e96d0e601c88c4e3aa53dbe135 Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Mon, 28 Feb 2022 23:31:03 +0900 Subject: [PATCH 23/29] add test for coverage --- .../aws-iotevents/test/detector-model.test.ts | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts index 1ea0270f971be..b7401f8867181 100644 --- a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts +++ b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts @@ -147,7 +147,19 @@ test('can set actions to events', () => { onEnter: [{ eventName: 'test-eventName1', condition: iotevents.Expression.currentInput(input), - actions: [{ renderActionConfig: () => ({ configuration: { setTimer: { timerName: 'test-timer' } } }) }], + actions: [{ + actionPolicies: [new iam.PolicyStatement({ + actions: ['lambda:InvokeFunction'], + resources: ['arn:aws:lambda:us-east-1:123456789012:function:MyFn'], + })], + renderActionConfig: () => ({ + configuration: { + lambda: { + functionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', + }, + }, + }), + }], }], }), }); @@ -159,13 +171,26 @@ test('can set actions to events', () => { Match.objectLike({ OnEnter: { Events: [{ - Actions: [{ SetTimer: { TimerName: 'test-timer' } }], + Actions: [{ Lambda: { FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn' } }], }], }, }), ], }, }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', + }], + }, + Roles: [{ + Ref: 'MyDetectorModelDetectorModelRoleF2FB4D88', + }], + }); }); test.each([ From b6860213f2d63608d14e5859bba36ec9ce4f953c Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Tue, 1 Mar 2022 10:06:17 +0900 Subject: [PATCH 24/29] write readme --- packages/@aws-cdk/aws-iot-actions/README.md | 264 +++++++++++++++++- .../@aws-cdk/aws-iotevents-actions/README.md | 30 ++ 2 files changed, 279 insertions(+), 15 deletions(-) diff --git a/packages/@aws-cdk/aws-iot-actions/README.md b/packages/@aws-cdk/aws-iot-actions/README.md index 94a5e848ce79f..088fda5f3e5b8 100644 --- a/packages/@aws-cdk/aws-iot-actions/README.md +++ b/packages/@aws-cdk/aws-iot-actions/README.md @@ -15,32 +15,266 @@ -This library contains integration classes to use a timer or set a variable, or send data to other AWS resources. -AWS IoT Events can trigger actions when it detects a specified event or transition event. +This library contains integration classes to send data to any number of +supported AWS Services. Instances of these classes should be passed to +`TopicRule` defined in `@aws-cdk/aws-iot`. Currently supported are: +- Republish a message to another MQTT topic - Invoke a Lambda function +- Put objects to a S3 bucket +- Put logs to CloudWatch Logs +- Capture CloudWatch metrics +- Change state for a CloudWatch alarm +- Put records to Kinesis Data stream +- Put records to Kinesis Data Firehose stream +- Send messages to SQS queues +- Publish messages on SNS topics + +## Republish a message to another MQTT topic + +The code snippet below creates an AWS IoT Rule that republish a message to +another MQTT topic when it is triggered. + +```ts +new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'"), + actions: [ + new actions.IotRepublishMqttAction('${topic()}/republish', { + qualityOfService: actions.MqttQualityOfService.AT_LEAST_ONCE, // optional property, default is MqttQualityOfService.ZERO_OR_MORE_TIMES + }), + ], +}); +``` ## Invoke a Lambda function -The code snippet below creates an Action that invoke a Lambda function +The code snippet below creates an AWS IoT Rule that invoke a Lambda function +when it is triggered. + +```ts +const func = 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); + };` + ), +}); + +new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id, timestamp() as timestamp, temperature FROM 'device/+/data'"), + actions: [new actions.LambdaFunctionAction(func)], +}); +``` + +## Put objects to a S3 bucket + +The code snippet below creates an AWS IoT Rule that put objects to a S3 bucket +when it is triggered. + +```ts +const bucket = new s3.Bucket(this, 'MyBucket'); + +new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + actions: [new actions.S3PutObjectAction(bucket)], +}); +``` + +The property `key` of `S3PutObjectAction` is given the value `${topic()}/${timestamp()}` by default. This `${topic()}` +and `${timestamp()}` is called Substitution templates. For more information see +[this documentation](https://docs.aws.amazon.com/iot/latest/developerguide/iot-substitution-templates.html). +In above sample, `${topic()}` is replaced by a given MQTT topic as `device/001/data`. And `${timestamp()}` is replaced +by the number of the current timestamp in milliseconds as `1636289461203`. So if the MQTT broker receives an MQTT topic +`device/001/data` on `2021-11-07T00:00:00.000Z`, the S3 bucket object will be put to `device/001/data/1636243200000`. + +You can also set specific `key` as following: + +```ts +const bucket = new s3.Bucket(this, 'MyBucket'); + +new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323( + "SELECT topic(2) as device_id, year, month, day FROM 'device/+/data'", + ), + actions: [ + new actions.S3PutObjectAction(bucket, { + key: '${year}/${month}/${day}/${topic(2)}', + }), + ], +}); +``` + +If you wanna set access control to the S3 bucket object, you can specify `accessControl` as following: + +```ts +const bucket = new s3.Bucket(this, 'MyBucket'); + +new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT * FROM 'device/+/data'"), + actions: [ + new actions.S3PutObjectAction(bucket, { + accessControl: s3.BucketAccessControl.PUBLIC_READ, + }), + ], +}); +``` + +## Put logs to CloudWatch Logs + +The code snippet below creates an AWS IoT Rule that put logs to CloudWatch Logs +when it is triggered. + +```ts +import * as logs from '@aws-cdk/aws-logs'; + +const logGroup = new logs.LogGroup(this, 'MyLogGroup'); + +new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + actions: [new actions.CloudWatchLogsAction(logGroup)], +}); +``` + +## Capture CloudWatch metrics + +The code snippet below creates an AWS IoT Rule that capture CloudWatch metrics when it is triggered. ```ts -import * as iotevents from '@aws-cdk/aws-iotevents'; -import * as actions from '@aws-cdk/aws-iotevents-actions'; -import * as lambda from '@aws-cdk/aws-lambda'; +const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323( + "SELECT topic(2) as device_id, namespace, unit, value, timestamp FROM 'device/+/data'", + ), + actions: [ + new actions.CloudWatchPutMetricAction({ + metricName: '${topic(2)}', + metricNamespace: '${namespace}', + metricUnit: '${unit}', + metricValue: '${value}', + metricTimestamp: '${timestamp}', + }), + ], +}); +``` + +## Change the state of an Amazon CloudWatch alarm + +The code snippet below creates an AWS IoT Rule that changes the state of an Amazon CloudWatch alarm when it is triggered: + +```ts +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; + +const metric = new cloudwatch.Metric({ + namespace: 'MyNamespace', + metricName: 'MyMetric', + dimensions: { MyDimension: 'MyDimensionValue' }, +}); +const alarm = new cloudwatch.Alarm(this, 'MyAlarm', { + metric: metric, + threshold: 100, + evaluationPeriods: 3, + datapointsToAlarm: 2, +}); + +const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT topic(2) as device_id FROM 'device/+/data'"), + actions: [ + new actions.CloudWatchSetAlarmStateAction(alarm, { + reason: 'AWS Iot Rule action is triggered', + alarmStateToSet: cloudwatch.AlarmState.ALARM, + }), + ], +}); +``` + +## Put records to Kinesis Data stream + +The code snippet below creates an AWS IoT Rule that put records to Kinesis Data +stream when it is triggered. + +```ts +import * as kinesis from '@aws-cdk/aws-kinesis'; + +const stream = new kinesis.Stream(this, 'MyStream'); + +const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT * FROM 'device/+/data'"), + actions: [ + new actions.KinesisPutRecordAction(stream, { + partitionKey: '${newuuid()}', + }), + ], +}); +``` + +## Put records to Kinesis Data Firehose stream + +The code snippet below creates an AWS IoT Rule that put records to Put records +to Kinesis Data Firehose stream when it is triggered. + +```ts +import * as firehose from '@aws-cdk/aws-kinesisfirehose'; +import * as destinations from '@aws-cdk/aws-kinesisfirehose-destinations'; + +const bucket = new s3.Bucket(this, 'MyBucket'); +const stream = new firehose.DeliveryStream(this, 'MyStream', { + destinations: [new destinations.S3Bucket(bucket)], +}); + +const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323("SELECT * FROM 'device/+/data'"), + actions: [ + new actions.FirehosePutRecordAction(stream, { + batchMode: true, + recordSeparator: actions.FirehoseRecordSeparator.NEWLINE, + }), + ], +}); +``` + +## Send messages to an SQS queue + +The code snippet below creates an AWS IoT Rule that send messages +to an SQS queue when it is triggered: + +```ts +import * as sqs from '@aws-cdk/aws-sqs'; + +const queue = new sqs.Queue(this, 'MyQueue'); + +const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323( + "SELECT topic(2) as device_id, year, month, day FROM 'device/+/data'", + ), + actions: [ + new actions.SqsQueueAction(queue, { + useBase64: true, // optional property, default is 'false' + }), + ], +}); +``` + +## Publish messages on an SNS topic + +The code snippet below creates and AWS IoT Rule that publishes messages to an SNS topic when it is triggered: + +```ts +import * as sns from '@aws-cdk/aws-sns'; -declare const input: iotevents.IInput; -declare const func: lambda.IFunction; +const topic = new sns.Topic(this, 'MyTopic'); -const state = new iotevents.State({ - stateName: 'MyState', - onEnter: [{ - eventName: 'test-event', - condition: iotevents.Expression.currentInput(input), - actions: [new actions.LambdaInvokeAction(func)], - }], +const topicRule = new iot.TopicRule(this, 'TopicRule', { + sql: iot.IotSql.fromStringAsVer20160323( + "SELECT topic(2) as device_id, year, month, day FROM 'device/+/data'", + ), + actions: [ + new actions.SnsTopicAction(topic, { + messageFormat: actions.SnsActionMessageFormat.JSON, // optional property, default is SnsActionMessageFormat.RAW + }), + ], }); ``` diff --git a/packages/@aws-cdk/aws-iotevents-actions/README.md b/packages/@aws-cdk/aws-iotevents-actions/README.md index 4ee5362b7cc9b..eb88dc82bb3c3 100644 --- a/packages/@aws-cdk/aws-iotevents-actions/README.md +++ b/packages/@aws-cdk/aws-iotevents-actions/README.md @@ -18,3 +18,33 @@ This library contains integration classes to specify actions of state events of Detector Model in `@aws-cdk/aws-iotevents`. Instances of these classes should be passed to `State` defined in `@aws-cdk/aws-iotevents` You can define built-in actions to use a timer or set a variable, or send data to other AWS resources. + +This library contains integration classes to use a timer or set a variable, or send data to other AWS resources. +AWS IoT Events can trigger actions when it detects a specified event or transition event. + +Currently supported are: + +- Invoke a Lambda function + +## Invoke a Lambda function + +The code snippet below creates an Action that invoke a Lambda function +when it is triggered. + +```ts +import * as iotevents from '@aws-cdk/aws-iotevents'; +import * as actions from '@aws-cdk/aws-iotevents-actions'; +import * as lambda from '@aws-cdk/aws-lambda'; + +declare const input: iotevents.IInput; +declare const func: lambda.IFunction; + +const state = new iotevents.State({ + stateName: 'MyState', + onEnter: [{ + eventName: 'test-event', + condition: iotevents.Expression.currentInput(input), + actions: [new actions.LambdaInvokeAction(func)], + }], +}); +``` From 369562dc634284d58917caf0694452baa7d7db85 Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Thu, 3 Mar 2022 20:02:26 +0900 Subject: [PATCH 25/29] address comments --- .../lib/lambda-invoke-action.ts | 16 +-- .../aws-iotevents-actions/package.json | 6 +- .../integ.lambda-invoke-action.expected.json | 33 +++--- .../test/lambda/integ.lambda-invoke-action.ts | 3 - .../test/lambda/lambda-invoke-action.test.ts | 5 +- packages/@aws-cdk/aws-iotevents/lib/action.ts | 16 ++- .../aws-iotevents/lib/detector-model.ts | 6 +- packages/@aws-cdk/aws-iotevents/lib/state.ts | 104 ++++++------------ .../aws-iotevents/test/detector-model.test.ts | 42 +------ .../test/integ.detector-model.expected.json | 76 ------------- .../test/integ.detector-model.ts | 18 +-- 11 files changed, 72 insertions(+), 253 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents-actions/lib/lambda-invoke-action.ts b/packages/@aws-cdk/aws-iotevents-actions/lib/lambda-invoke-action.ts index 4cbc04574622c..af9dec5d32472 100644 --- a/packages/@aws-cdk/aws-iotevents-actions/lib/lambda-invoke-action.ts +++ b/packages/@aws-cdk/aws-iotevents-actions/lib/lambda-invoke-action.ts @@ -1,29 +1,19 @@ -import * as iam from '@aws-cdk/aws-iam'; import * as iotevents from '@aws-cdk/aws-iotevents'; import * as lambda from '@aws-cdk/aws-lambda'; +import { Construct } from 'constructs'; /** * The action to write the data to an AWS Lambda function. */ export class LambdaInvokeAction implements iotevents.IAction { - /** - * The policies to perform the AWS IoT Events action. - */ - readonly actionPolicies?: iam.PolicyStatement[]; - /** * @param func the AWS Lambda function to be invoked by this action */ constructor(private readonly func: lambda.IFunction) { - this.actionPolicies = [ - new iam.PolicyStatement({ - actions: ['lambda:InvokeFunction'], - resources: [func.functionArn], - }), - ]; } - renderActionConfig(): iotevents.ActionConfig { + bind(_scope: Construct, options: iotevents.ActionBindOptions): iotevents.ActionConfig { + this.func.grantInvoke(options.role); return { configuration: { lambda: { diff --git a/packages/@aws-cdk/aws-iotevents-actions/package.json b/packages/@aws-cdk/aws-iotevents-actions/package.json index 48d8edc4a1569..90a925459abc9 100644 --- a/packages/@aws-cdk/aws-iotevents-actions/package.json +++ b/packages/@aws-cdk/aws-iotevents-actions/package.json @@ -81,14 +81,16 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-iotevents": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", - "@aws-cdk/core": "0.0.0" + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-iotevents": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", - "@aws-cdk/core": "0.0.0" + "@aws-cdk/core": "0.0.0", + "constructs": "^3.3.69" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" diff --git a/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.expected.json b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.expected.json index 50eab8e42c8fd..be70d65360d32 100644 --- a/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.expected.json +++ b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.expected.json @@ -116,6 +116,18 @@ "OnEnter": { "Events": [ { + "Actions": [ + { + "Lambda": { + "FunctionArn": { + "Fn::GetAtt": [ + "MyFunction3BAA72D1", + "Arn" + ] + } + } + } + ], "Condition": { "Fn::Join": [ "", @@ -132,26 +144,7 @@ } ] }, - "OnExit": {}, - "OnInput": { - "Events": [ - { - "Actions": [ - { - "Lambda": { - "FunctionArn": { - "Fn::GetAtt": [ - "MyFunction3BAA72D1", - "Arn" - ] - } - } - } - ], - "EventName": "test-input-event" - } - ] - }, + "OnInput": {}, "StateName": "MyState" } ] diff --git a/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.ts b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.ts index 9a57f27c22b1a..2084f2cb7bd9c 100644 --- a/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.ts +++ b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.ts @@ -32,9 +32,6 @@ class TestStack extends cdk.Stack { onEnter: [{ eventName: 'test-event', condition: iotevents.Expression.currentInput(input), - }], - onInput: [{ - eventName: 'test-input-event', actions: [new actions.LambdaInvokeAction(func)], }], }); diff --git a/packages/@aws-cdk/aws-iotevents-actions/test/lambda/lambda-invoke-action.test.ts b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/lambda-invoke-action.test.ts index 5bf872958d5ef..493114dbd3bb5 100644 --- a/packages/@aws-cdk/aws-iotevents-actions/test/lambda/lambda-invoke-action.test.ts +++ b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/lambda-invoke-action.test.ts @@ -10,7 +10,10 @@ let func: lambda.IFunction; beforeEach(() => { stack = new cdk.Stack(); input = iotevents.Input.fromInputName(stack, 'MyInput', 'test-input'); - func = lambda.Function.fromFunctionArn(stack, 'MyFunction', 'arn:aws:lambda:us-east-1:123456789012:function:MyFn'); + func = lambda.Function.fromFunctionAttributes(stack, 'MyFunction', { + functionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', + sameEnvironment: true, + }); }); test('Default property', () => { diff --git a/packages/@aws-cdk/aws-iotevents/lib/action.ts b/packages/@aws-cdk/aws-iotevents/lib/action.ts index 245f8ba46acec..1358d232da15a 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/action.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/action.ts @@ -1,19 +1,25 @@ import * as iam from '@aws-cdk/aws-iam'; +import { Construct } from 'constructs'; import { CfnDetectorModel } from './iotevents.generated'; /** - * An abstract action for DetectorModel. + * TODO: */ -export interface IAction { +export interface ActionBindOptions { /** - * The policies to perform the AWS IoT Events action. + * TODO: */ - readonly actionPolicies?: iam.PolicyStatement[]; + readonly role: iam.IRole; +} +/** + * An abstract action for DetectorModel. + */ +export interface IAction { /** * Returns the AWS IoT Events action specification. */ - renderActionConfig(): ActionConfig; + bind(scope: Construct, options: ActionBindOptions): ActionConfig; } /** diff --git a/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts b/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts index 6f7f8488b2f45..1545e8ec69446 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/detector-model.ts @@ -117,10 +117,6 @@ export class DetectorModel extends Resource implements IDetectorModel { assumedBy: new iam.ServicePrincipal('iotevents.amazonaws.com'), }); - props.initialState._collectPolicies().forEach(policy => { - role.addToPrincipalPolicy(policy); - }); - const resource = new CfnDetectorModel(this, 'Resource', { detectorModelName: this.physicalName, detectorModelDescription: props.description, @@ -128,7 +124,7 @@ export class DetectorModel extends Resource implements IDetectorModel { key: props.detectorKey, detectorModelDefinition: { initialStateName: props.initialState.stateName, - states: props.initialState._collectStateJsons(), + states: props.initialState._collectStateJsons(this, { role }, new Set()), }, roleArn: role.roleArn, }); diff --git a/packages/@aws-cdk/aws-iotevents/lib/state.ts b/packages/@aws-cdk/aws-iotevents/lib/state.ts index a9b298ee626c1..37afd1ebda187 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/state.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -1,5 +1,5 @@ -import * as iam from '@aws-cdk/aws-iam'; -import { IAction } from './action'; +import { Construct } from 'constructs'; +import { IAction, ActionBindOptions } from './action'; import { Event } from './event'; import { Expression } from './expression'; import { CfnDetectorModel } from './iotevents.generated'; @@ -66,28 +66,12 @@ export interface StateProps { readonly stateName: string; /** - * Specifies the events on enter. The conditions of the events will be evaluated when entering this state. - * If the condition of the event evaluates to `true`, the actions of the event will be executed. + * Specifies the events on enter. the conditions of the events are evaluated when the state is entered. + * If the condition is `TRUE`, the actions of the event are performed. * - * @default - no events will trigger on entering this state + * @default - events on enter will not be set */ readonly onEnter?: Event[]; - - /** - * Specifies the events on input. The conditions of the events will be evaluated when any input is received. - * If the condition of the event evaluates to `true`, the actions of the event will be executed. - * - * @default - no events will trigger on input in this state - */ - readonly onInput?: Event[]; - - /** - * Specifies the events on exit. The conditions of the events are evaluated when an exiting this state. - * If the condition evaluates to `true`, the actions of the event will be executed. - * - * @default - no events will trigger on exiting this state - */ - readonly onExit?: Event[]; } /** @@ -129,28 +113,22 @@ export class State { /** * Collect states in dependency gragh that constructed by state transitions, * and return the JSONs of the states. + * This function is called recursively and collect the states. * * @internal */ - public _collectStateJsons(): CfnDetectorModel.StateProperty[] { - return this.collectStates(new Set()).map(state => state.toStateJson()); - } + public _collectStateJsons(scope: Construct, actionBindOptions: ActionBindOptions, collectedStates: Set): CfnDetectorModel.StateProperty[] { + if (collectedStates.has(this)) { + return []; + } + collectedStates.add(this); - /** - * Collect policies to perform the actions in dependency gragh that constructed by state transitions. - * - * @internal - */ - public _collectPolicies(): iam.PolicyStatement[] { - return this.collectStates(new Set()) - .flatMap(state => ([ - ...state.props.onEnter ?? [], - ...state.props.onInput ?? [], - ...state.props.onExit ?? [], - ...state.transitionEvents, - ])) - .flatMap(event => event.actions ?? []) - .flatMap(action => action.actionPolicies ?? []); + return [ + this.toStateJson(scope, actionBindOptions), + ...this.transitionEvents.flatMap(transitionEvent => { + return transitionEvent.nextState._collectStateJsons(scope, actionBindOptions, collectedStates); + }), + ]; } /** @@ -162,51 +140,35 @@ export class State { return this.props.onEnter?.some(event => event.condition) ?? false; } - private collectStates(collectedStates: Set): State[] { - if (collectedStates.has(this)) { - return []; - } - collectedStates.add(this); - - return [ - this, - ...this.transitionEvents.flatMap(transitionEvent => { - return transitionEvent.nextState.collectStates(collectedStates); - }), - ]; - } - - private toStateJson(): CfnDetectorModel.StateProperty { - const { onEnter, onInput, onExit } = this.props; + private toStateJson(scope: Construct, actionBindOptions: ActionBindOptions): CfnDetectorModel.StateProperty { + const { onEnter } = this.props; return { stateName: this.stateName, - onEnter: { - events: toEventsJson(onEnter), - }, + onEnter: onEnter && { events: toEventsJson(scope, actionBindOptions, onEnter) }, onInput: { - events: toEventsJson(onInput), - transitionEvents: toTransitionEventsJson(this.transitionEvents), - }, - onExit: { - events: toEventsJson(onExit), + transitionEvents: toTransitionEventsJson(scope, actionBindOptions, this.transitionEvents), }, }; } } -function toEventsJson(events?: Event[]): CfnDetectorModel.EventProperty[] | undefined { - if (!events) { - return undefined; - } - +function toEventsJson( + scope: Construct, + actionBindOptions: ActionBindOptions, + events: Event[], +): CfnDetectorModel.EventProperty[] | undefined { return events.map(event => ({ eventName: event.eventName, condition: event.condition?.evaluate(), - actions: event.actions?.map(action => action.renderActionConfig().configuration), + actions: event.actions?.map(action => action.bind(scope, actionBindOptions).configuration), })); } -function toTransitionEventsJson(transitionEvents: TransitionEvent[]): CfnDetectorModel.TransitionEventProperty[] | undefined { +function toTransitionEventsJson( + scope: Construct, + actionBindOptions: ActionBindOptions, + transitionEvents: TransitionEvent[], +): CfnDetectorModel.TransitionEventProperty[] | undefined { if (transitionEvents.length === 0) { return undefined; } @@ -214,7 +176,7 @@ function toTransitionEventsJson(transitionEvents: TransitionEvent[]): CfnDetecto return transitionEvents.map(transitionEvent => ({ eventName: transitionEvent.eventName, condition: transitionEvent.condition.evaluate(), - actions: transitionEvent.actions?.map(action => action.renderActionConfig().configuration), + actions: transitionEvent.actions?.map(action => action.bind(scope, actionBindOptions).configuration), nextState: transitionEvent.nextState.stateName, })); } diff --git a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts index b7401f8867181..ce8389c92e50d 100644 --- a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts +++ b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts @@ -148,11 +148,7 @@ test('can set actions to events', () => { eventName: 'test-eventName1', condition: iotevents.Expression.currentInput(input), actions: [{ - actionPolicies: [new iam.PolicyStatement({ - actions: ['lambda:InvokeFunction'], - resources: ['arn:aws:lambda:us-east-1:123456789012:function:MyFn'], - })], - renderActionConfig: () => ({ + bind: () => ({ configuration: { lambda: { functionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', @@ -178,40 +174,6 @@ test('can set actions to events', () => { ], }, }); - - Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [{ - Action: 'lambda:InvokeFunction', - Effect: 'Allow', - Resource: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', - }], - }, - Roles: [{ - Ref: 'MyDetectorModelDetectorModelRoleF2FB4D88', - }], - }); -}); - -test.each([ - ['onInput', { onInput: [{ eventName: 'test-eventName1' }] }, { OnInput: { Events: [{ EventName: 'test-eventName1' }] } }], - ['onExit', { onExit: [{ eventName: 'test-eventName1' }] }, { OnExit: { Events: [{ EventName: 'test-eventName1' }] } }], -])('can set %s to State', (_, events, expected) => { - // WHEN - new iotevents.DetectorModel(stack, 'MyDetectorModel', { - initialState: new iotevents.State({ - stateName: 'test-state', - onEnter: [{ eventName: 'test-eventName1', condition: iotevents.Expression.currentInput(input) }], - ...events, - }), - }); - - // THEN - Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { - DetectorModelDefinition: { - States: [Match.objectLike(expected)], - }, - }); }); test('can set states with transitions', () => { @@ -316,7 +278,7 @@ test('can set actions to transitions', () => { iotevents.Expression.inputAttribute(input, 'payload.temperature'), iotevents.Expression.fromString('12'), ), - executing: [{ renderActionConfig: () => ({ configuration: { setTimer: { timerName: 'test-timer' } } }) }], + executing: [{ bind: () => ({ configuration: { setTimer: { timerName: 'test-timer' } } }) }], }); new iotevents.DetectorModel(stack, 'MyDetectorModel', { diff --git a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json index c04885e5480df..d7ec550052f8f 100644 --- a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json +++ b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json @@ -82,81 +82,7 @@ } ] }, - "OnExit": { - "Events": [ - { - "Actions": [ - { - "SetVariable": { - "Value": { - "Fn::Join": [ - "", - [ - "$input.", - { - "Ref": "MyInput08947B23" - }, - ".payload.temperature" - ] - ] - }, - "VariableName": "temperature" - } - } - ], - "Condition": { - "Fn::Join": [ - "", - [ - "$input.", - { - "Ref": "MyInput08947B23" - }, - ".payload.temperature == 31.7" - ] - ] - }, - "EventName": "test-exit-event" - } - ] - }, "OnInput": { - "Events": [ - { - "Actions": [ - { - "SetVariable": { - "Value": { - "Fn::Join": [ - "", - [ - "$input.", - { - "Ref": "MyInput08947B23" - }, - ".payload.temperature" - ] - ] - }, - "VariableName": "temperature" - } - } - ], - "Condition": { - "Fn::Join": [ - "", - [ - "$input.", - { - "Ref": "MyInput08947B23" - }, - ".payload.temperature == 31.6" - ] - ] - }, - "EventName": "test-input-event" - } - ], "TransitionEvents": [ { "Actions": [ @@ -198,8 +124,6 @@ "StateName": "online" }, { - "OnEnter": {}, - "OnExit": {}, "OnInput": { "TransitionEvents": [ { diff --git a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts index 95af14ddeaea8..e469be1a6620d 100644 --- a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts +++ b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts @@ -19,7 +19,7 @@ class TestStack extends cdk.Stack { }); const setTemperatureAction: iotevents.IAction = { - renderActionConfig: () => ({ + bind: () => ({ configuration: { setVariable: { variableName: 'temperature', @@ -43,22 +43,6 @@ class TestStack extends cdk.Stack { ), actions: [setTemperatureAction], }], - onInput: [{ - eventName: 'test-input-event', - condition: iotevents.Expression.eq( - iotevents.Expression.inputAttribute(input, 'payload.temperature'), - iotevents.Expression.fromString('31.6'), - ), - actions: [setTemperatureAction], - }], - onExit: [{ - eventName: 'test-exit-event', - condition: iotevents.Expression.eq( - iotevents.Expression.inputAttribute(input, 'payload.temperature'), - iotevents.Expression.fromString('31.7'), - ), - actions: [setTemperatureAction], - }], }); const offlineState = new iotevents.State({ stateName: 'offline', From 9f7ddf77c18e495c1816fbe131deecd2073f5794 Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Thu, 3 Mar 2022 20:51:34 +0900 Subject: [PATCH 26/29] documentation --- packages/@aws-cdk/aws-iotevents/README.md | 171 ++++-------------- packages/@aws-cdk/aws-iotevents/lib/action.ts | 4 +- 2 files changed, 33 insertions(+), 142 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index c58549922b9cc..d89c27c7e2f18 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -40,180 +40,71 @@ Import it into your code: import * as iotevents from '@aws-cdk/aws-iotevents'; ``` -## Input +## `DetectorModel` -You can create `Input` as following. You can put messages to the Input with AWS IoT Core Topic Rule, AWS IoT Analytics and more. -For more information, see [the documentation](https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-getting-started.html). +The following example creates an AWS IoT Events detector model to your stack. +The detector model need a reference to at least one AWS IoT Events input. +AWS IoT Events inputs enable the detector to get MQTT payload values from IoT Core rules. ```ts import * as iotevents from '@aws-cdk/aws-iotevents'; +import * as actions from '@aws-cdk/aws-iotevents-actions'; +import * as lambda from '@aws-cdk/aws-lambda'; + +declare const func: lambda.IFunction; const input = new iotevents.Input(this, 'MyInput', { inputName: 'my_input', // optional attributeJsonPaths: ['payload.deviceId', 'payload.temperature'], }); -``` - -To grant permissions to put messages in the Input, -you can use the `grantWrite()` method: - -```ts -import * as iam from '@aws-cdk/aws-iam'; -import * as iotevents from '@aws-cdk/aws-iotevents'; - -declare const grantable: iam.IGrantable; -declare const input: iotevents.IInput; -input.grantWrite(grantable); -``` - -## State - -You can create `State` as following. -If a State is used for a Detector Model's initial State, it's required that its `onEnter` Event is non-null, -and contains a reference to an Input via the `condition` property. -And if a message is put to the Input, the detector instances are created regardless of the evaluation result of `condition`. -You can set the reference to Input with `iotevents.Expression.currentInput()` or `iotevents.Expression.inputAttribute()`. -In other states, `onEnter` is optional. - -```ts -import * as iotevents from '@aws-cdk/aws-iotevents'; - -declare const input: iotevents.IInput; - -const initialState = new iotevents.State({ - stateName: 'MyState', +const warmState = new iotevents.State({ + stateName: 'warm', onEnter: [{ - eventName: 'onEnter', + eventName: 'test-event', condition: iotevents.Expression.currentInput(input), + actions: [new actions.LambdaInvokeAction(func)], // optional }], }); -``` - -You can set actions on the `onEnter` event. They are performed if `condition` evaluates to `true`. -If you omit `condition`, actions are performed every time the State is entered. -For more information, see [supported actions](https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-supported-actions.html). - -```ts -import * as iotevents from '@aws-cdk/aws-iotevents'; - -declare const input: iotevents.IInput; - -const setTemperatureAction = { - renderActionConfig: () => ({ - configuration: { - setVariable: { - variableName: 'temperature', - value: iotevents.Expression.inputAttribute(input, 'payload.temperature').evaluate(), - }, - }, - }), -}; - -const state = new iotevents.State({ - stateName: 'MyState', - onEnter: [{ // optional - eventName: 'onEnter', - actions: [setTemperatureAction], // optional - condition: iotevents.Expression.currentInput(input), // optional - }], -}); -``` - -You can also use the `onInput` and `onExit` properties. -`onInput` is triggered when messages are put to the Input that is referenced from the detector model. -`onExit` is triggered when exiting this State. - -```ts -import * as iotevents from '@aws-cdk/aws-iotevents'; - -const state = new iotevents.State({ - stateName: 'warm', - onEnter: [{ // optional - eventName: 'onEnter', - }], - onInput: [{ // optional - eventName: 'onInput', - }], - onExit: [{ // optional - eventName: 'onExit', - }], +const coldState = new iotevents.State({ + stateName: 'cold', }); -``` -You can set transitions of the states as following: - -```ts -import * as iotevents from '@aws-cdk/aws-iotevents'; - -declare const input: iotevents.IInput; -declare const action: iotevents.IAction; -declare const stateA: iotevents.State; -declare const stateB: iotevents.State; - -// transit from stateA to stateB when temperature is 10 -stateA.transitionTo(stateB, { +// transit to coldState when temperature is 10 +warmState.transitionTo(coldState, { eventName: 'to_coldState', // optional property, default by combining the names of the States when: iotevents.Expression.eq( iotevents.Expression.inputAttribute(input, 'payload.temperature'), iotevents.Expression.fromString('10'), ), - executing: [action], // optional, + executing: [new actions.LambdaInvokeAction(func)], // optional +}); +// transit to warmState when temperature is 20 +coldState.transitionTo(warmState, { + when: iotevents.Expression.eq( + iotevents.Expression.inputAttribute(input, 'payload.temperature'), + iotevents.Expression.fromString('20'), + ), }); -``` - -## DetectorModel - -You can create a Detector Model as follows: - -```ts -import * as iotevents from '@aws-cdk/aws-iotevents'; - -declare const state: iotevents.State; new iotevents.DetectorModel(this, 'MyDetectorModel', { detectorModelName: 'test-detector-model', // optional description: 'test-detector-model-description', // optional property, default is none evaluationMethod: iotevents.EventEvaluation.SERIAL, // optional property, default is iotevents.EventEvaluation.BATCH detectorKey: 'payload.deviceId', // optional property, default is none and single detector instance will be created and all inputs will be routed to it - initialState: state, + initialState: warmState, }); ``` -## Examples - -The following example creates an AWS IoT Events detector model to your stack. -The State of this detector model transits according to the temperature. +To grant permissions to put messages in the input, +you can use the `grantWrite()` method: ```ts +import * as iam from '@aws-cdk/aws-iam'; import * as iotevents from '@aws-cdk/aws-iotevents'; -const input = new iotevents.Input(this, 'MyInput', { - attributeJsonPaths: ['payload.deviceId', 'payload.temperature'], -}); - -const warmState = new iotevents.State({ - stateName: 'warm', - onEnter: [{ - eventName: 'onEnter', - condition: iotevents.Expression.currentInput(input), - }], -}); -const coldState = new iotevents.State({ - stateName: 'cold', -}); - -const temperatureEqual = (temperature: string) => - iotevents.Expression.eq( - iotevents.Expression.inputAttribute(input, 'payload.temperature'), - iotevents.Expression.fromString('10'), - ) - -warmState.transitionTo(coldState, { when: temperatureEqual('10') }); -coldState.transitionTo(warmState, { when: temperatureEqual('20') }); +declare const grantable: iam.IGrantable; +const input = iotevents.Input.fromInputName(this, 'MyInput', 'my_input'); -new iotevents.DetectorModel(this, 'MyDetectorModel', { - detectorKey: 'payload.deviceId', - initialState: warmState, -}); +input.grantWrite(grantable); ``` diff --git a/packages/@aws-cdk/aws-iotevents/lib/action.ts b/packages/@aws-cdk/aws-iotevents/lib/action.ts index 1358d232da15a..f43c6b6c91626 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/action.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/action.ts @@ -3,11 +3,11 @@ import { Construct } from 'constructs'; import { CfnDetectorModel } from './iotevents.generated'; /** - * TODO: + * Options when binding a Action to a detector model. */ export interface ActionBindOptions { /** - * TODO: + * The IAM role assumed by IoT Events to perform the action. */ readonly role: iam.IRole; } From c0ccdf8d9aabe0db81477b5c7b41a5b85bd46a18 Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Fri, 4 Mar 2022 10:03:35 +0900 Subject: [PATCH 27/29] address commits --- packages/@aws-cdk/aws-iotevents/lib/state.ts | 2 +- .../test/integ.detector-model.expected.json | 38 ------------------- .../test/integ.detector-model.ts | 23 +---------- 3 files changed, 3 insertions(+), 60 deletions(-) diff --git a/packages/@aws-cdk/aws-iotevents/lib/state.ts b/packages/@aws-cdk/aws-iotevents/lib/state.ts index 37afd1ebda187..0159628c4a4ff 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/state.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -156,7 +156,7 @@ function toEventsJson( scope: Construct, actionBindOptions: ActionBindOptions, events: Event[], -): CfnDetectorModel.EventProperty[] | undefined { +): CfnDetectorModel.EventProperty[] { return events.map(event => ({ eventName: event.eventName, condition: event.condition?.evaluate(), diff --git a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json index d7ec550052f8f..888869a41e68e 100644 --- a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json +++ b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.expected.json @@ -43,25 +43,6 @@ "OnEnter": { "Events": [ { - "Actions": [ - { - "SetVariable": { - "Value": { - "Fn::Join": [ - "", - [ - "$input.", - { - "Ref": "MyInput08947B23" - }, - ".payload.temperature" - ] - ] - }, - "VariableName": "temperature" - } - } - ], "Condition": { "Fn::Join": [ "", @@ -85,25 +66,6 @@ "OnInput": { "TransitionEvents": [ { - "Actions": [ - { - "SetVariable": { - "Value": { - "Fn::Join": [ - "", - [ - "$input.", - { - "Ref": "MyInput08947B23" - }, - ".payload.temperature" - ] - ] - }, - "VariableName": "temperature" - } - } - ], "Condition": { "Fn::Join": [ "", diff --git a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts index e469be1a6620d..5f6d2839f3a93 100644 --- a/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts +++ b/packages/@aws-cdk/aws-iotevents/test/integ.detector-model.ts @@ -1,11 +1,3 @@ -/** - * Stack verification steps: - * * put a message - * * aws iotevents-data batch-put-message --messages=messageId=(date | md5),inputName=test_input,payload=(echo '{"payload":{"temperature":31.9,"deviceId":"000"}}' | base64) - * * describe the detector - * * aws iotevents-data describe-detector --detector-model-name test-detector-model --key-value=000 - * * verify `stateName` and `variables` of the detector - */ import * as cdk from '@aws-cdk/core'; import * as iotevents from '../lib'; @@ -18,17 +10,6 @@ class TestStack extends cdk.Stack { attributeJsonPaths: ['payload.deviceId', 'payload.temperature'], }); - const setTemperatureAction: iotevents.IAction = { - bind: () => ({ - configuration: { - setVariable: { - variableName: 'temperature', - value: iotevents.Expression.inputAttribute(input, 'payload.temperature').evaluate(), - }, - }, - }), - }; - const onlineState = new iotevents.State({ stateName: 'online', onEnter: [{ @@ -41,20 +22,20 @@ class TestStack extends cdk.Stack { iotevents.Expression.fromString('31.5'), ), ), - actions: [setTemperatureAction], }], }); const offlineState = new iotevents.State({ stateName: 'offline', }); + // 1st => 2nd onlineState.transitionTo(offlineState, { when: iotevents.Expression.eq( iotevents.Expression.inputAttribute(input, 'payload.temperature'), iotevents.Expression.fromString('12'), ), - executing: [setTemperatureAction], }); + // 2st => 1st offlineState.transitionTo(onlineState, { when: iotevents.Expression.eq( iotevents.Expression.inputAttribute(input, 'payload.temperature'), From f9b8d8ca541843864577fe015698f8c8074bf69e Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Fri, 4 Mar 2022 10:09:39 +0900 Subject: [PATCH 28/29] add link of actions to README --- packages/@aws-cdk/aws-iotevents/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index d89c27c7e2f18..0c7e491c65fae 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -46,6 +46,9 @@ The following example creates an AWS IoT Events detector model to your stack. The detector model need a reference to at least one AWS IoT Events input. AWS IoT Events inputs enable the detector to get MQTT payload values from IoT Core rules. +You can define built-in actions to use a timer or set a variable, or send data to other AWS resources. +See also [@aws-cdk/aws-iotevents-actions](https://docs.aws.amazon.com/cdk/api/v1/docs/aws-iotevents-actions-readme.html) for other actions. + ```ts import * as iotevents from '@aws-cdk/aws-iotevents'; import * as actions from '@aws-cdk/aws-iotevents-actions'; From ea3059dd66d2095a516986a7847ef8e01608af2f Mon Sep 17 00:00:00 2001 From: yamatatsu Date: Fri, 4 Mar 2022 12:38:27 +0900 Subject: [PATCH 29/29] test behavior of action --- .../aws-iotevents/test/detector-model.test.ts | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts index ce8389c92e50d..ec754f25c1aad 100644 --- a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts +++ b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts @@ -176,6 +176,101 @@ test('can set actions to events', () => { }); }); + +test('can set an action to multiple detector models', () => { + // GIVEN an action + const action: iotevents.IAction = { + bind: (_, { role }) => { + role.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['lambda:InvokeFunction'], + resources: ['arn:aws:lambda:us-east-1:123456789012:function:MyFn'], + })); + return { + configuration: { + lambda: { functionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn' }, + }, + }; + }, + }; + + // WHEN the action is set to two detector models + new iotevents.DetectorModel(stack, 'MyDetectorModel1', { + detectorModelName: 'MyDetectorModel1', + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName1', + condition: iotevents.Expression.currentInput(input), + actions: [action], + }], + }), + }); + new iotevents.DetectorModel(stack, 'MyDetectorModel2', { + detectorModelName: 'MyDetectorModel2', + initialState: new iotevents.State({ + stateName: 'test-state', + onEnter: [{ + eventName: 'test-eventName1', + condition: iotevents.Expression.currentInput(input), + actions: [action], + }], + }), + }); + + // THEN creates two detector model resouces and two iam policy resources + Template.fromStack(stack).resourceCountIs('AWS::IoTEvents::DetectorModel', 2); + Template.fromStack(stack).resourceCountIs('AWS::IAM::Policy', 2); + + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelName: 'MyDetectorModel1', + DetectorModelDefinition: { + States: [ + Match.objectLike({ + OnEnter: { + Events: [{ + Actions: [{ Lambda: { FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn' } }], + }], + }, + }), + ], + }, + }); + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelName: 'MyDetectorModel2', + DetectorModelDefinition: { + States: [ + Match.objectLike({ + OnEnter: { + Events: [{ + Actions: [{ Lambda: { FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn' } }], + }], + }, + }), + ], + }, + }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + Roles: [{ Ref: 'MyDetectorModel1DetectorModelRoleB36845CD' }], + PolicyDocument: { + Statement: [{ + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', + }], + }, + }); + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + Roles: [{ Ref: 'MyDetectorModel2DetectorModelRole3C437E90' }], + PolicyDocument: { + Statement: [{ + Action: 'lambda:InvokeFunction', + Effect: 'Allow', + Resource: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', + }], + }, + }); +}); + test('can set states with transitions', () => { // GIVEN const firstState = new iotevents.State({