From 3f0c040f2d79098f9c0db9a084fe500de4327d65 Mon Sep 17 00:00:00 2001 From: Tatsuya Yamamoto Date: Sat, 5 Mar 2022 06:34:36 +0900 Subject: [PATCH] feat(iotevents): support actions (#18869) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR allow IoT Events detector model to perform actions as [this documentation](https://docs.aws.amazon.com/iotevents/latest/developerguide/iotevents-supported-actions.html). This PR is in roadmap of https://github.com/aws/aws-cdk/issues/17711. ![スクリーンショット 2022-02-08 22 43 33](https://user-images.githubusercontent.com/11013683/152999288-81721f15-fefb-4108-b34b-aab3f88a7ab8.png) With this fix, all the interfaces of the DetectorModel are now implemented! And next works is implementing expressions and actions. The exapmle in readme became not simple, so also this PR has sorted explanation of readme. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-iotevents-actions/README.md | 30 +++ .../aws-iotevents-actions/lib/index.ts | 3 +- .../lib/lambda-invoke-action.ts | 25 +++ .../aws-iotevents-actions/package.json | 12 +- .../test/iotevents-actions.test.ts | 6 - .../integ.lambda-invoke-action.expected.json | 162 ++++++++++++++++ .../test/lambda/integ.lambda-invoke-action.ts | 48 +++++ .../test/lambda/lambda-invoke-action.test.ts | 64 +++++++ packages/@aws-cdk/aws-iotevents/README.md | 9 + packages/@aws-cdk/aws-iotevents/lib/action.ts | 33 ++++ .../aws-iotevents/lib/detector-model.ts | 2 +- packages/@aws-cdk/aws-iotevents/lib/event.ts | 12 +- packages/@aws-cdk/aws-iotevents/lib/index.ts | 1 + packages/@aws-cdk/aws-iotevents/lib/state.ts | 53 ++++-- .../aws-iotevents/test/detector-model.test.ts | 173 ++++++++++++++++++ tools/@aws-cdk/pkglint/lib/rules.ts | 1 + 16 files changed, 608 insertions(+), 26 deletions(-) create mode 100644 packages/@aws-cdk/aws-iotevents-actions/lib/lambda-invoke-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/lambda/integ.lambda-invoke-action.expected.json create mode 100644 packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.ts create mode 100644 packages/@aws-cdk/aws-iotevents-actions/test/lambda/lambda-invoke-action.test.ts create mode 100644 packages/@aws-cdk/aws-iotevents/lib/action.ts 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)], + }], +}); +``` diff --git a/packages/@aws-cdk/aws-iotevents-actions/lib/index.ts b/packages/@aws-cdk/aws-iotevents-actions/lib/index.ts index 3e4b0ef0a73d4..4b2ec39329315 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 './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..af9dec5d32472 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/lib/lambda-invoke-action.ts @@ -0,0 +1,25 @@ +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 { + /** + * @param func the AWS Lambda function to be invoked by this action + */ + constructor(private readonly func: lambda.IFunction) { + } + + bind(_scope: Construct, options: iotevents.ActionBindOptions): iotevents.ActionConfig { + this.func.grantInvoke(options.role); + return { + configuration: { + lambda: { + functionArn: this.func.functionArn, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-iotevents-actions/package.json b/packages/@aws-cdk/aws-iotevents-actions/package.json index 457af7c3df4ae..90a925459abc9 100644 --- a/packages/@aws-cdk/aws-iotevents-actions/package.json +++ b/packages/@aws-cdk/aws-iotevents-actions/package.json @@ -78,11 +78,19 @@ "jest": "^27.5.1" }, "dependencies": { - "@aws-cdk/core": "0.0.0" + "@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", + "constructs": "^3.3.69" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { - "@aws-cdk/core": "0.0.0" + "@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", + "constructs": "^3.3.69" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" 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/lambda/integ.lambda-invoke-action.expected.json b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.expected.json new file mode 100644 index 0000000000000..be70d65360d32 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.expected.json @@ -0,0 +1,162 @@ +{ + "Resources": { + "MyInput08947B23": { + "Type": "AWS::IoTEvents::Input", + "Properties": { + "InputDefinition": { + "Attributes": [ + { + "JsonPath": "payload.deviceId" + } + ] + }, + "InputName": "test_input" + } + }, + "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", + "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": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyFunction3BAA72D1", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyDetectorModelDetectorModelRoleDefaultPolicy82887422", + "Roles": [ + { + "Ref": "MyDetectorModelDetectorModelRoleF2FB4D88" + } + ] + } + }, + "MyDetectorModel559C0B0E": { + "Type": "AWS::IoTEvents::DetectorModel", + "Properties": { + "DetectorModelDefinition": { + "InitialStateName": "MyState", + "States": [ + { + "OnEnter": { + "Events": [ + { + "Actions": [ + { + "Lambda": { + "FunctionArn": { + "Fn::GetAtt": [ + "MyFunction3BAA72D1", + "Arn" + ] + } + } + } + ], + "Condition": { + "Fn::Join": [ + "", + [ + "currentInput(\"", + { + "Ref": "MyInput08947B23" + }, + "\")" + ] + ] + }, + "EventName": "test-event" + } + ] + }, + "OnInput": {}, + "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/lambda/integ.lambda-invoke-action.ts b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.ts new file mode 100644 index 0000000000000..2084f2cb7bd9c --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/integ.lambda-invoke-action.ts @@ -0,0 +1,48 @@ +/** + * 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) + * * verify that the lambda logs be put + */ +import * as iotevents from '@aws-cdk/aws-iotevents'; +import * as lambda from '@aws-cdk/aws-lambda'; +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 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', + onEnter: [{ + eventName: 'test-event', + condition: iotevents.Expression.currentInput(input), + actions: [new actions.LambdaInvokeAction(func)], + }], + }); + + new iotevents.DetectorModel(this, 'MyDetectorModel', { + detectorKey: 'payload.deviceId', + initialState: state, + }); + } +} + +const app = new cdk.App(); +new TestStack(app, 'lambda-invoke-action-test-stack'); +app.synth(); 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 new file mode 100644 index 0000000000000..493114dbd3bb5 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents-actions/test/lambda/lambda-invoke-action.test.ts @@ -0,0 +1,64 @@ +import { Template } from '@aws-cdk/assertions'; +import * as iotevents from '@aws-cdk/aws-iotevents'; +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 func: lambda.IFunction; +beforeEach(() => { + stack = new cdk.Stack(); + input = iotevents.Input.fromInputName(stack, 'MyInput', 'test-input'); + func = lambda.Function.fromFunctionAttributes(stack, 'MyFunction', { + functionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', + sameEnvironment: true, + }); +}); + +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.LambdaInvokeAction(func)], + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [{ + OnEnter: { + Events: [{ + Actions: [{ + Lambda: { + FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', + }, + }], + }], + }, + }], + }, + RoleArn: { + 'Fn::GetAtt': ['MyDetectorModelDetectorModelRoleF2FB4D88', 'Arn'], + }, + }); + + 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', + }], + }); +}); diff --git a/packages/@aws-cdk/aws-iotevents/README.md b/packages/@aws-cdk/aws-iotevents/README.md index 809bac071ef7d..0c7e491c65fae 100644 --- a/packages/@aws-cdk/aws-iotevents/README.md +++ b/packages/@aws-cdk/aws-iotevents/README.md @@ -46,8 +46,15 @@ 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'; +import * as lambda from '@aws-cdk/aws-lambda'; + +declare const func: lambda.IFunction; const input = new iotevents.Input(this, 'MyInput', { inputName: 'my_input', // optional @@ -59,6 +66,7 @@ const warmState = new iotevents.State({ onEnter: [{ eventName: 'test-event', condition: iotevents.Expression.currentInput(input), + actions: [new actions.LambdaInvokeAction(func)], // optional }], }); const coldState = new iotevents.State({ @@ -72,6 +80,7 @@ warmState.transitionTo(coldState, { iotevents.Expression.inputAttribute(input, 'payload.temperature'), iotevents.Expression.fromString('10'), ), + executing: [new actions.LambdaInvokeAction(func)], // optional }); // transit to warmState when temperature is 20 coldState.transitionTo(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..f43c6b6c91626 --- /dev/null +++ b/packages/@aws-cdk/aws-iotevents/lib/action.ts @@ -0,0 +1,33 @@ +import * as iam from '@aws-cdk/aws-iam'; +import { Construct } from 'constructs'; +import { CfnDetectorModel } from './iotevents.generated'; + +/** + * Options when binding a Action to a detector model. + */ +export interface ActionBindOptions { + /** + * The IAM role assumed by IoT Events to perform the action. + */ + readonly role: iam.IRole; +} + +/** + * An abstract action for DetectorModel. + */ +export interface IAction { + /** + * Returns the AWS IoT Events action specification. + */ + bind(scope: Construct, options: ActionBindOptions): ActionConfig; +} + +/** + * Properties for a AWS IoT Events 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..1545e8ec69446 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._collectStateJsons(this, { role }, 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 610469db9c32c..fd452686e054e 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/event.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/event.ts @@ -1,7 +1,8 @@ +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 { /** @@ -10,9 +11,16 @@ 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) */ readonly condition?: Expression; + + /** + * The actions to be performed. + * + * @default - no actions will be performed + */ + 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..0159628c4a4ff 100644 --- a/packages/@aws-cdk/aws-iotevents/lib/state.ts +++ b/packages/@aws-cdk/aws-iotevents/lib/state.ts @@ -1,3 +1,5 @@ +import { Construct } from 'constructs'; +import { IAction, ActionBindOptions } from './action'; import { Event } from './event'; import { Expression } from './expression'; import { CfnDetectorModel } from './iotevents.generated'; @@ -15,13 +17,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 with the transition. + * + * @default - no actions will be performed + */ + 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 { /** @@ -30,12 +39,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 next state to transit to. When the resuld of condition expression is TRUE, the state is transited. + * The actions to be performed. + * + * @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. */ readonly nextState: State; } @@ -75,7 +91,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 @@ -90,6 +106,7 @@ export class State { eventName: options.eventName ?? `${this.stateName}_to_${targetState.stateName}`, nextState: targetState, condition: options.when, + actions: options.executing, }); } @@ -100,16 +117,16 @@ export class State { * * @internal */ - public _collectStateJsons(collectedStates: Set): CfnDetectorModel.StateProperty[] { + public _collectStateJsons(scope: Construct, actionBindOptions: ActionBindOptions, collectedStates: Set): CfnDetectorModel.StateProperty[] { if (collectedStates.has(this)) { return []; } collectedStates.add(this); return [ - this.toStateJson(), + this.toStateJson(scope, actionBindOptions), ...this.transitionEvents.flatMap(transitionEvent => { - return transitionEvent.nextState._collectStateJsons(collectedStates); + return transitionEvent.nextState._collectStateJsons(scope, actionBindOptions, collectedStates); }), ]; } @@ -123,26 +140,35 @@ export class State { return this.props.onEnter?.some(event => event.condition) ?? false; } - private toStateJson(): CfnDetectorModel.StateProperty { + private toStateJson(scope: Construct, actionBindOptions: ActionBindOptions): CfnDetectorModel.StateProperty { const { onEnter } = this.props; return { stateName: this.stateName, - onEnter: onEnter && { events: toEventsJson(onEnter) }, + onEnter: onEnter && { events: toEventsJson(scope, actionBindOptions, onEnter) }, onInput: { - transitionEvents: toTransitionEventsJson(this.transitionEvents), + transitionEvents: toTransitionEventsJson(scope, actionBindOptions, this.transitionEvents), }, }; } } -function toEventsJson(events: Event[]): CfnDetectorModel.EventProperty[] { +function toEventsJson( + scope: Construct, + actionBindOptions: ActionBindOptions, + events: Event[], +): CfnDetectorModel.EventProperty[] { return events.map(event => ({ eventName: event.eventName, condition: event.condition?.evaluate(), + 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; } @@ -150,6 +176,7 @@ function toTransitionEventsJson(transitionEvents: TransitionEvent[]): CfnDetecto return transitionEvents.map(transitionEvent => ({ eventName: transitionEvent.eventName, condition: transitionEvent.condition.evaluate(), + 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 c90a10cf34374..ec754f25c1aad 100644 --- a/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts +++ b/packages/@aws-cdk/aws-iotevents/test/detector-model.test.ts @@ -139,6 +139,138 @@ 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: [{ + bind: () => ({ + configuration: { + lambda: { + functionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn', + }, + }, + }), + }], + }], + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IoTEvents::DetectorModel', { + DetectorModelDefinition: { + States: [ + Match.objectLike({ + OnEnter: { + Events: [{ + Actions: [{ Lambda: { FunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:MyFn' } }], + }], + }, + }), + ], + }, + }); +}); + + +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({ @@ -222,6 +354,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: [{ bind: () => ({ 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/tools/@aws-cdk/pkglint/lib/rules.ts b/tools/@aws-cdk/pkglint/lib/rules.ts index 9010f7edb2527..bb76c64b3205b 100644 --- a/tools/@aws-cdk/pkglint/lib/rules.ts +++ b/tools/@aws-cdk/pkglint/lib/rules.ts @@ -1762,6 +1762,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'];