forked from aws/aws-cdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
StepFunctionsRestApi implemented along with unit and integration test…
…ing. Fixed Integration test and generated expected json for stepFunctionsRestApi Stack deployment. Added code snippet to the README. Removing restApiprops option as the composition in StepFunctionsRestApiProps. Added Error for when state machine is not of type EXPRESS Added Context to input with includeRequestContext boolean varibale to pass requestContext to State Machine input. closes aws#15081.
- Loading branch information
Saqib Dhuka
committed
Oct 23, 2021
1 parent
924045d
commit 562dd22
Showing
10 changed files
with
1,208 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
177 changes: 177 additions & 0 deletions
177
packages/@aws-cdk/aws-apigateway/lib/integrations/stepFunctions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import * as iam from '@aws-cdk/aws-iam'; | ||
import * as sfn from '@aws-cdk/aws-stepfunctions'; | ||
import { Token } from '@aws-cdk/core'; | ||
import { IntegrationConfig, IntegrationOptions, PassthroughBehavior } from '../integration'; | ||
import { Method } from '../method'; | ||
import { AwsIntegration } from './aws'; | ||
|
||
/** | ||
* StepFunctionsIntegrationOptions | ||
*/ | ||
export interface StepFunctionsIntegrationOptions extends IntegrationOptions { | ||
/** | ||
* Use proxy integration or normal (request/response mapping) integration. | ||
* | ||
* @default false | ||
*/ | ||
readonly proxy?: boolean; | ||
|
||
/** | ||
* Check if cors is enabled | ||
* @default false | ||
*/ | ||
readonly corsEnabled?: boolean; | ||
|
||
/** | ||
* Check if requestContext is enabled | ||
* If enabled, requestContext is passed into the input of the State Machine. | ||
* @default false | ||
*/ | ||
readonly includeRequestContext?: boolean; | ||
|
||
} | ||
/** | ||
* Integrates a Synchronous Express State Machine from AWS Step Functions to an API Gateway method. | ||
* | ||
* @example | ||
* | ||
* const handler = new sfn.StateMachine(this, 'MyStateMachine', ...); | ||
* api.addMethod('GET', new StepFunctionsIntegration(handler)); | ||
* | ||
*/ | ||
|
||
export class StepFunctionsIntegration extends AwsIntegration { | ||
private readonly handler: sfn.IStateMachine; | ||
|
||
constructor(handler: sfn.IStateMachine, options: StepFunctionsIntegrationOptions = { }) { | ||
|
||
const integResponse = getIntegrationResponse(); | ||
const requestTemplate = getRequestTemplates(handler, options.includeRequestContext); | ||
|
||
if (options.corsEnabled) { | ||
super({ | ||
proxy: options.proxy, | ||
service: 'states', | ||
action: 'StartSyncExecution', | ||
options, | ||
}); | ||
} else { | ||
super({ | ||
proxy: options.proxy, | ||
service: 'states', | ||
action: 'StartSyncExecution', | ||
options: { | ||
credentialsRole: options.credentialsRole, | ||
integrationResponses: integResponse, | ||
passthroughBehavior: PassthroughBehavior.NEVER, | ||
requestTemplates: requestTemplate, | ||
}, | ||
}); | ||
} | ||
|
||
this.handler = handler; | ||
} | ||
|
||
public bind(method: Method): IntegrationConfig { | ||
const bindResult = super.bind(method); | ||
const principal = new iam.ServicePrincipal('apigateway.amazonaws.com'); | ||
|
||
this.handler.grantExecution(principal, 'states:StartSyncExecution'); | ||
|
||
let stateMachineName; | ||
|
||
if (this.handler instanceof sfn.StateMachine) { | ||
//if not imported, extract the name from the CFN layer to reach the | ||
//literal value if it is given (rather than a token) | ||
stateMachineName = (this.handler.node.defaultChild as sfn.CfnStateMachine).stateMachineName; | ||
} else { | ||
stateMachineName = 'StateMachine-' + (String(this.handler.stack.node.addr).substring(0, 8)); | ||
} | ||
|
||
let deploymentToken; | ||
|
||
if (!Token.isUnresolved(stateMachineName)) { | ||
deploymentToken = JSON.stringify({ stateMachineName }); | ||
} | ||
return { | ||
...bindResult, | ||
deploymentToken, | ||
}; | ||
|
||
} | ||
} | ||
|
||
function getIntegrationResponse() { | ||
const errorResponse = [ | ||
{ | ||
selectionPattern: '4\\d{2}', | ||
statusCode: '400', | ||
responseTemplates: { | ||
'application/json': `{ | ||
"error": "Bad input!" | ||
}`, | ||
}, | ||
}, | ||
{ | ||
selectionPattern: '5\\d{2}', | ||
statusCode: '500', | ||
responseTemplates: { | ||
'application/json': '"error": $input.path(\'$.error\')', | ||
}, | ||
}, | ||
]; | ||
|
||
const integResponse = [ | ||
{ | ||
statusCode: '200', | ||
responseTemplates: { | ||
'application/json': `#set($inputRoot = $input.path('$')) | ||
#if($input.path('$.status').toString().equals("FAILED")) | ||
#set($context.responseOverride.status = 500) | ||
{ | ||
"error": "$input.path('$.error')", | ||
"cause": "$input.path('$.cause')" | ||
} | ||
#else | ||
$input.path('$.output') | ||
#end`, | ||
}, | ||
}, | ||
...errorResponse, | ||
]; | ||
|
||
return integResponse; | ||
} | ||
|
||
function getRequestTemplates(handler: sfn.IStateMachine, includeRequestContext: boolean | undefined) { | ||
const templateString = getTemplateString(handler, includeRequestContext); | ||
|
||
const requestTemplate: { [contentType:string] : string } = | ||
{ | ||
'application/json': templateString, | ||
}; | ||
|
||
return requestTemplate; | ||
} | ||
|
||
function getTemplateString(handler: sfn.IStateMachine, includeRequestContext: boolean | undefined): string { | ||
let templateString: string; | ||
const requestContextStr:string = '"body": $util.escapeJavaScript($input.json(\'$\')),"requestContext": {"accountId" : "$context.identity.accountId","apiId" : "$context.apiId","apiKey" : "$context.identity.apiKey","authorizerPrincipalId" : "$context.authorizer.principalId","caller" : "$context.identity.caller","cognitoAuthenticationProvider" : "$context.identity.cognitoAuthenticationProvider","cognitoAuthenticationType" : "$context.identity.cognitoAuthenticationType","cognitoIdentityId" : "$context.identity.cognitoIdentityId","cognitoIdentityPoolId" : "$context.identity.cognitoIdentityPoolId","httpMethod" : "$context.httpMethod","stage" : "$context.stage","sourceIp" : "$context.identity.sourceIp","user" : "$context.identity.user","userAgent" : "$context.identity.userAgent","userArn" : "$context.identity.userArn","requestId" : "$context.requestId","resourceId" : "$context.resourceId","resourcePath" : "$context.resourcePath"}'; | ||
const search = '"'; | ||
const replaceWith = '\\"'; | ||
if (typeof includeRequestContext === 'boolean' && includeRequestContext === true) { | ||
templateString = ` | ||
#set($allParams = $input.params()) | ||
{ | ||
"input": "{${requestContextStr.split(search).join(replaceWith)}}", | ||
"stateMachineArn": "${handler.stateMachineArn}" | ||
}`; | ||
} else { | ||
templateString = ` | ||
#set($inputRoot = $input.path('$')) { | ||
"input": "{\\"body\\": $util.escapeJavaScript($input.json('$'))}", | ||
"stateMachineArn": "${handler.stateMachineArn}" | ||
}`; | ||
} | ||
return templateString; | ||
} |
130 changes: 130 additions & 0 deletions
130
packages/@aws-cdk/aws-apigateway/lib/stepFunctions-api.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import * as iam from '@aws-cdk/aws-iam'; | ||
import * as sfn from '@aws-cdk/aws-stepfunctions'; | ||
import { Construct } from 'constructs'; | ||
import { RestApi, RestApiProps } from '.'; | ||
import { StepFunctionsIntegration } from './integrations/stepFunctions'; | ||
import { Model } from './model'; | ||
|
||
/** | ||
* StepFunctionsRestApiProps | ||
*/ | ||
export interface StepFunctionsRestApiProps extends RestApiProps { | ||
/** | ||
* The default State Machine that handles all requests from this API. | ||
* | ||
* This handler will be used as a the default integration for all methods in | ||
* this API, unless specified otherwise in `addMethod`. | ||
*/ | ||
readonly handler: sfn.IStateMachine; | ||
|
||
/** | ||
* If true, route all requests to the State Machine | ||
* | ||
* If set to false, you will need to explicitly define the API model using | ||
* `addResource` and `addMethod` (or `addProxy`). | ||
* | ||
* Note: Proxy is not yet supported for State Machines | ||
* | ||
* @default false | ||
*/ | ||
readonly proxy?: boolean; | ||
|
||
/** | ||
* Check if requestContext is enabled | ||
* If enabled, requestContext is passed into the input of the State Machine. | ||
* @default false | ||
*/ | ||
readonly includeRequestContext?: boolean; | ||
} | ||
|
||
/** | ||
* Defines an API Gateway REST API with a Synchrounous Express State Machine as a proxy integration. | ||
* | ||
*/ | ||
|
||
export class StepFunctionsRestApi extends RestApi { | ||
constructor(scope: Construct, id: string, props: StepFunctionsRestApiProps) { | ||
if (props.defaultIntegration) { | ||
throw new Error('Cannot specify "defaultIntegration" since Step Functions integration is automatically defined'); | ||
} | ||
|
||
if ((props.handler.node.defaultChild as sfn.CfnStateMachine).stateMachineType !== sfn.StateMachineType.EXPRESS) { | ||
throw new Error('State Machine must be of type "EXPRESS". Please use StateMachineType.EXPRESS as the stateMachineType'); | ||
} | ||
const apiRole = getRole(scope, props); | ||
const methodResp = getMethodResponse(); | ||
|
||
let corsEnabled; | ||
|
||
if (props.defaultCorsPreflightOptions !== undefined) { | ||
corsEnabled = true; | ||
} else { | ||
corsEnabled = false; | ||
} | ||
|
||
super(scope, id, { | ||
defaultIntegration: new StepFunctionsIntegration(props.handler, { | ||
credentialsRole: apiRole, | ||
proxy: false, //proxy not avaialble for Step Functions yet | ||
corsEnabled: corsEnabled, | ||
includeRequestContext: props.includeRequestContext, | ||
}), | ||
...props, | ||
}); | ||
|
||
this.root.addMethod('ANY', new StepFunctionsIntegration(props.handler, { | ||
credentialsRole: apiRole, | ||
includeRequestContext: props.includeRequestContext, | ||
}), { | ||
methodResponses: [ | ||
...methodResp, | ||
], | ||
}); | ||
} | ||
} | ||
|
||
function getRole(scope: Construct, props: StepFunctionsRestApiProps): iam.Role { | ||
const apiName: string = props.handler + '-apiRole'; | ||
const apiRole = new iam.Role(scope, apiName, { | ||
assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), | ||
}); | ||
|
||
apiRole.attachInlinePolicy( | ||
new iam.Policy(scope, 'AllowStartSyncExecution', { | ||
statements: [ | ||
new iam.PolicyStatement({ | ||
actions: ['states:StartSyncExecution'], | ||
effect: iam.Effect.ALLOW, | ||
resources: [props.handler.stateMachineArn], | ||
}), | ||
], | ||
}), | ||
); | ||
|
||
return apiRole; | ||
} | ||
|
||
function getMethodResponse() { | ||
const methodResp = [ | ||
{ | ||
statusCode: '200', | ||
responseModels: { | ||
'application/json': Model.EMPTY_MODEL, | ||
}, | ||
}, | ||
{ | ||
statusCode: '400', | ||
responseModels: { | ||
'application/json': Model.ERROR_MODEL, | ||
}, | ||
}, | ||
{ | ||
statusCode: '500', | ||
responseModels: { | ||
'application/json': Model.ERROR_MODEL, | ||
}, | ||
}, | ||
]; | ||
|
||
return methodResp; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.