diff --git a/README.md b/README.md index 301834a0..fadb729f 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ This is the Serverless Framework plugin for AWS Step Functions. - [Events](#events) - [API Gateway](#api-gateway) - [Simple HTTP endpoint](#simple-http-endpoint) + - [Custom Step Functions Action](#custom-step-functions-action) - [HTTP Endpoint with custom IAM Role](#http-endpoint-with-custom-iam-role) - [Share API Gateway and API Resources](#share-api-gateway-and-api-resources) - [Enabling CORS](#enabling-cors) @@ -375,6 +376,40 @@ stepFunctions: definition: ``` +#### Custom Step Functions Action + +Step Functions have custom actions like DescribeExecution or StopExecution to fetch and control them. You can use custom actions like this: + +```yml +stepFunctions: + stateMachines: + start: + events: + - http: + path: action/start + method: POST + definition: + ... + status: + events: + - http: + path: action/status + method: POST + action: DescribeExecution + definition: + ... + stop: + events: + - http: + path: action/stop + method: POST + action: StopExecution + definition: + ... +``` + +Request template is not used when action is set because there're a bunch of actions. However if you want to use request template you can use [Customizing request body mapping templates](#customizing-request-body-mapping-templates). + #### HTTP Endpoint with custom IAM Role The plugin would generate an IAM Role for you by default. However, if you wish to use an IAM role that you have provisioned separately, then you can override the IAM Role like this: diff --git a/lib/deploy/events/apiGateway/apigateway-to-stepfunctions-assume-role.json b/lib/deploy/events/apiGateway/apigateway-to-stepfunctions-assume-role.json deleted file mode 100644 index 6f1d8d1d..00000000 --- a/lib/deploy/events/apiGateway/apigateway-to-stepfunctions-assume-role.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": "apigateway.amazonaws.com" - }, - "Action": "sts:AssumeRole" - } - ] - }, - "Policies": [ - { - "PolicyName": "apigatewaytostepfunctions", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "states:StartExecution" - ], - "Resource": "*" - } - ] - } - } - ] - } -} diff --git a/lib/deploy/events/apiGateway/iamRole.js b/lib/deploy/events/apiGateway/iamRole.js index 4946d72b..3320bf06 100644 --- a/lib/deploy/events/apiGateway/iamRole.js +++ b/lib/deploy/events/apiGateway/iamRole.js @@ -1,7 +1,6 @@ 'use strict'; const _ = require('lodash'); const BbPromise = require('bluebird'); -const path = require('path'); module.exports = { compileHttpIamRole() { @@ -14,15 +13,58 @@ module.exports = { return BbPromise.resolve(); } - const iamRoleApiGatewayToStepFunctionsTemplate = - JSON.stringify(this.serverless.utils.readFileSync( - path.join(__dirname, - 'apigateway-to-stepfunctions-assume-role.json')) - ); + const iamRoleApiGatewayToStepFunctionsAction = [ + 'states:StartExecution', + ]; + + // generate IAM Role action by http.action parameter. + this.pluginhttpValidated.events.forEach((event) => { + if (_.has(event, 'http.action')) { + const actionName = `states:${event.http.action}`; + + if (iamRoleApiGatewayToStepFunctionsAction.indexOf(actionName) === -1) { + iamRoleApiGatewayToStepFunctionsAction.push(actionName); + } + } + }); + + const iamRoleApiGatewayToStepFunctions = { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: 'apigateway.amazonaws.com', + }, + Action: 'sts:AssumeRole', + }, + ], + }, + Policies: [ + { + PolicyName: 'apigatewaytostepfunctions', + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Action: iamRoleApiGatewayToStepFunctionsAction, + Resource: '*', + }, + ], + }, + }, + ], + }, + }; + const getApiToStepFunctionsIamRoleLogicalId = this.getApiToStepFunctionsIamRoleLogicalId(); const newIamRoleStateMachineExecutionObject = { - [getApiToStepFunctionsIamRoleLogicalId]: JSON.parse(iamRoleApiGatewayToStepFunctionsTemplate), + [getApiToStepFunctionsIamRoleLogicalId]: iamRoleApiGatewayToStepFunctions, }; _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, diff --git a/lib/deploy/events/apiGateway/iamRole.test.js b/lib/deploy/events/apiGateway/iamRole.test.js index cad54d49..59d504b1 100644 --- a/lib/deploy/events/apiGateway/iamRole.test.js +++ b/lib/deploy/events/apiGateway/iamRole.test.js @@ -120,4 +120,56 @@ describe('#compileHttpIamRole()', () => { expect(resources).to.not.haveOwnProperty('ApigatewayToStepFunctionsRole'); }); }); + + it('Should add DescribeExecution action when it is assigned in config', () => { + serverlessStepFunctions.pluginhttpValidated = { + events: [ + { + stateMachineName: 'first', + http: { + path: 'foo/bar1', + method: 'post', + }, + }, + { + stateMachineName: 'first', + http: { + path: 'foo/bar2', + method: 'post', + action: 'DescribeExecution', + }, + }, + ], + }; + + serverlessStepFunctions + .compileHttpIamRole().then(() => { + const properties = serverlessStepFunctions.serverless.service.provider + .compiledCloudFormationTemplate.Resources.ApigatewayToStepFunctionsRole.Properties; + expect(properties.Policies[0].PolicyDocument.Statement[0].Action) + .to.deep.equal(['states:StartExecution', 'states:DescribeExecution']); + }); + }); + + it('Should not add DescribeExecution action when it is not assigned in config', () => { + serverlessStepFunctions.pluginhttpValidated = { + events: [ + { + stateMachineName: 'first', + http: { + path: 'foo/bar1', + method: 'post', + }, + }, + ], + }; + + serverlessStepFunctions + .compileHttpIamRole().then(() => { + const properties = serverlessStepFunctions.serverless.service.provider + .compiledCloudFormationTemplate.Resources.ApigatewayToStepFunctionsRole.Properties; + expect(properties.Policies[0].PolicyDocument.Statement[0].Action) + .to.deep.equal(['states:StartExecution']); + }); + }); }); diff --git a/lib/deploy/events/apiGateway/methods.js b/lib/deploy/events/apiGateway/methods.js index 6310aa93..d64810b3 100644 --- a/lib/deploy/events/apiGateway/methods.js +++ b/lib/deploy/events/apiGateway/methods.js @@ -150,6 +150,13 @@ module.exports = { ], }; const iamRole = _.get(http, 'iamRole', defaultIamRole); + let integrationAction = ':states:action/StartExecution'; + let passthroughBehavior = 'NEVER'; + if (http && http.action) { + integrationAction = `:states:action/${http.action}`; + passthroughBehavior = 'WHEN_NO_TEMPLATES'; + } + const integration = { IntegrationHttpMethod: 'POST', Type: 'AWS', @@ -162,11 +169,11 @@ module.exports = { { Ref: 'AWS::Region', }, - ':states:action/StartExecution', + integrationAction, ], ], }, - PassthroughBehavior: 'NEVER', + PassthroughBehavior: passthroughBehavior, RequestTemplates: this.getIntegrationRequestTemplates( stateMachineName, stateMachineObj, @@ -234,6 +241,12 @@ module.exports = { http.request.template ); } + + if (_.has(http, 'action')) { + // no template if some action was defined. + return {}; + } + // default template return defaultTemplate; }, diff --git a/lib/deploy/events/apiGateway/methods.test.js b/lib/deploy/events/apiGateway/methods.test.js index 37cf9929..eec5341c 100644 --- a/lib/deploy/events/apiGateway/methods.test.js +++ b/lib/deploy/events/apiGateway/methods.test.js @@ -131,6 +131,19 @@ describe('#methods()', () => { .ResponseParameters['method.response.header.Access-Control-Allow-Origin']) .to.equal('\'*\''); }); + + it('should change passthroughBehavior and action when action is set', + () => { + expect(serverlessStepFunctions.getMethodIntegration('stateMachine', 'custom', { + action: 'DescribeExecution', + }).Properties.Integration.PassthroughBehavior) + .to.equal('WHEN_NO_TEMPLATES'); + + expect(serverlessStepFunctions.getMethodIntegration('stateMachine', 'custom', { + action: 'DescribeExecution', + }).Properties.Integration.Uri['Fn::Join'][1][2]) + .to.equal(':states:action/DescribeExecution'); + }); }); describe('#getIntegrationRequestTemplates()', () => {