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'];