Skip to content

Commit ef176a9

Browse files
author
Saqib Dhuka
committed
StepFunctionsRestApi implemented along with unit and integration testing.
Fixed Integration test and generated expected json for stepFunctionsRestApi Stack deployment. closes #15081.
1 parent 969673c commit ef176a9

File tree

9 files changed

+1007
-2
lines changed

9 files changed

+1007
-2
lines changed

packages/@aws-cdk/aws-apigateway/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export * from './authorizers';
2121
export * from './access-log';
2222
export * from './api-definition';
2323
export * from './gateway-response';
24+
export * from './stepFunctions-api';
2425

2526
// AWS::ApiGateway CloudFormation Resources:
2627
export * from './apigateway.generated';

packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './aws';
22
export * from './lambda';
33
export * from './http';
44
export * from './mock';
5+
export * from './stepFunctions';
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import * as iam from '@aws-cdk/aws-iam';
2+
import * as sfn from '@aws-cdk/aws-stepfunctions';
3+
import { Token } from '@aws-cdk/core';
4+
import { IntegrationConfig, IntegrationOptions, PassthroughBehavior } from '../integration';
5+
import { Method } from '../method';
6+
import { AwsIntegration } from './aws';
7+
8+
/**
9+
* StepFunctionsIntegrationOptions
10+
*/
11+
export interface StepFunctionsIntegrationOptions extends IntegrationOptions{
12+
/**
13+
* Use proxy integration or normal (request/response mapping) integration.
14+
*
15+
* @default false
16+
*/
17+
readonly proxy?: boolean;
18+
19+
/**
20+
* Check if cors is enabled
21+
* @default false
22+
*/
23+
readonly corsEnabled?: boolean;
24+
25+
}
26+
/**
27+
* Integrates an AWS Step Functions to an API Gateway method.
28+
*
29+
* @example
30+
*
31+
* const handler = new sfn.StateMachine(this, 'MyStateMachine', ...);
32+
* api.addMethod('GET', new StepFunctionsIntegration(handler));
33+
*
34+
*/
35+
36+
export class StepFunctionsIntegration extends AwsIntegration {
37+
private readonly handler: sfn.IStateMachine;
38+
39+
constructor(handler: sfn.IStateMachine, options: StepFunctionsIntegrationOptions = { }) {
40+
41+
const integResponse = getIntegrationResponse();
42+
const requestTemplate = getRequestTemplates(handler);
43+
44+
if (options.corsEnabled) {
45+
super({
46+
proxy: options.proxy,
47+
service: 'states',
48+
action: 'StartSyncExecution',
49+
options,
50+
});
51+
} else {
52+
super({
53+
proxy: options.proxy,
54+
service: 'states',
55+
action: 'StartSyncExecution',
56+
options: {
57+
credentialsRole: options.credentialsRole,
58+
integrationResponses: integResponse,
59+
passthroughBehavior: PassthroughBehavior.NEVER,
60+
requestTemplates: requestTemplate,
61+
},
62+
});
63+
}
64+
65+
this.handler = handler;
66+
}
67+
68+
public bind(method: Method): IntegrationConfig {
69+
const bindResult = super.bind(method);
70+
const principal = new iam.ServicePrincipal('apigateway.amazonaws.com');
71+
72+
this.handler.grantExecution(principal, 'states:StartSyncExecution');
73+
74+
let stateMachineName;
75+
76+
if (this.handler instanceof sfn.StateMachine) {
77+
//if not imported, extract the name from the CFN layer to reach the
78+
//literal value if it is given (rather than a token)
79+
stateMachineName = (this.handler.node.defaultChild as sfn.CfnStateMachine).stateMachineName;
80+
} else {
81+
// stateMachineName = 'NewStateMachine-' + String(this.handler.stateMachineArn);
82+
stateMachineName = 'StateMachine-' + (String(this.handler.stack.node.addr).substring(0, 8));
83+
}
84+
85+
let deploymentToken;
86+
87+
if (!Token.isUnresolved(stateMachineName)) {
88+
deploymentToken = JSON.stringify({ stateMachineName });
89+
}
90+
return {
91+
...bindResult,
92+
deploymentToken,
93+
};
94+
95+
}
96+
}
97+
98+
function getIntegrationResponse() {
99+
const errorResponse = [
100+
{
101+
selectionPattern: '4\\d{2}',
102+
statusCode: '400',
103+
responseTemplates: {
104+
'application/json': `{
105+
"error": "Bad input!"
106+
}`,
107+
},
108+
},
109+
{
110+
selectionPattern: '5\\d{2}',
111+
statusCode: '500',
112+
responseTemplates: {
113+
'application/json': '"error": $input.path(\'$.error\')',
114+
},
115+
},
116+
];
117+
118+
const integResponse = [
119+
{
120+
statusCode: '200',
121+
responseTemplates: {
122+
'application/json': `#set($inputRoot = $input.path('$'))
123+
#if($input.path('$.status').toString().equals("FAILED"))
124+
#set($context.responseOverride.status = 500)
125+
"error":$input.path('$.error')
126+
#else
127+
$input.path('$.output')
128+
#end`,
129+
},
130+
},
131+
...errorResponse,
132+
];
133+
134+
return integResponse;
135+
}
136+
137+
function getRequestTemplates(handler: sfn.IStateMachine) {
138+
const templateString = getTemplateString(handler);
139+
const requestTemplate: { [contenType:string] : string } =
140+
{
141+
'application/json': templateString,
142+
};
143+
144+
return requestTemplate;
145+
}
146+
147+
function getTemplateString(handler: sfn.IStateMachine): string {
148+
// const setStr: string = "#set($inputRoot = $input.path('$')) {";
149+
// const inputStr: string = '"input": "$util.escapeJavaScript($input.json(\'$\'))",';
150+
// const stateMachineStr: string = '"stateMachineArn":"';
151+
// const endStr: string = '"}';
152+
// const templateString: string = setStr + inputStr + stateMachineStr + String(handler.stateMachineArn) + endStr;
153+
154+
const templateString: string = `#set($inputRoot = $input.path('$')) {
155+
"input": "$util.escapeJavaScript($input.json(\'$\'))",
156+
"stateMachineArn":${handler.stateMachineArn}}`;
157+
158+
return templateString;
159+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import * as iam from '@aws-cdk/aws-iam';
2+
import * as sfn from '@aws-cdk/aws-stepfunctions';
3+
import { Construct } from 'constructs';
4+
import { RestApi, RestApiProps } from '.';
5+
import { StepFunctionsIntegration } from './integrations/stepFunctions';
6+
import { Model } from './model';
7+
8+
/**
9+
* StepFunctionsRestApiProps
10+
*/
11+
export interface StepFunctionsRestApiProps extends RestApiProps {
12+
/**
13+
* The default State Machine that handles all requests from this API.
14+
*
15+
* This handler will be used as a the default integration for all methods in
16+
* this API, unless specified otherwise in `addMethod`.
17+
*/
18+
readonly handler: sfn.IStateMachine;
19+
20+
/**
21+
* If true, route all requests to the State Machine
22+
*
23+
* If set to false, you will need to explicitly define the API model using
24+
* `addResource` and `addMethod` (or `addProxy`).
25+
*
26+
* @default true
27+
*/
28+
readonly proxy?: boolean;
29+
30+
/**
31+
* Rest API props options
32+
* @default - no options.
33+
*
34+
*/
35+
readonly options?: RestApiProps;
36+
}
37+
38+
/**
39+
* Defines an API Gateway REST API with AWS Step Functions proxy integration.
40+
*
41+
*/
42+
43+
export class StepFunctionsRestApi extends RestApi {
44+
constructor(scope: Construct, id: string, props: StepFunctionsRestApiProps) {
45+
if ((props.options && props.options.defaultIntegration) || props.defaultIntegration) {
46+
throw new Error('Cannot specify "defaultIntegration" since Step Functions integration is automatically defined');
47+
}
48+
49+
const apiRole = getRole(scope, props);
50+
const methodResp = getMethodResponse();
51+
52+
let corsEnabled;
53+
54+
if (props.defaultCorsPreflightOptions !== undefined) {
55+
corsEnabled = true;
56+
} else {
57+
corsEnabled = false;
58+
}
59+
60+
super(scope, id, {
61+
defaultIntegration: new StepFunctionsIntegration(props.handler, {
62+
credentialsRole: apiRole,
63+
proxy: false, //proxy not avaialble for Step Functions yet
64+
corsEnabled: corsEnabled,
65+
}),
66+
...props.options,
67+
...props,
68+
});
69+
70+
this.root.addMethod('ANY', new StepFunctionsIntegration(props.handler, {
71+
credentialsRole: apiRole,
72+
}), {
73+
methodResponses: [
74+
...methodResp,
75+
],
76+
});
77+
}
78+
}
79+
80+
function getRole(scope: Construct, props: StepFunctionsRestApiProps): iam.Role {
81+
const apiName: string = props.handler + '-apiRole';
82+
const apiRole = new iam.Role(scope, apiName, {
83+
assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
84+
});
85+
86+
apiRole.attachInlinePolicy(
87+
new iam.Policy(scope, 'AllowStartSyncExecution', {
88+
statements: [
89+
new iam.PolicyStatement({
90+
actions: ['states:StartSyncExecution'],
91+
effect: iam.Effect.ALLOW,
92+
resources: [props.handler.stateMachineArn],
93+
}),
94+
],
95+
}),
96+
);
97+
98+
return apiRole;
99+
}
100+
101+
function getMethodResponse() {
102+
const methodResp = [
103+
{
104+
statusCode: '200',
105+
responseModels: {
106+
'application/json': Model.EMPTY_MODEL,
107+
},
108+
},
109+
{
110+
statusCode: '400',
111+
responseModels: {
112+
'application/json': Model.ERROR_MODEL,
113+
},
114+
},
115+
{
116+
statusCode: '500',
117+
responseModels: {
118+
'application/json': Model.ERROR_MODEL,
119+
},
120+
},
121+
];
122+
123+
return methodResp;
124+
}

packages/@aws-cdk/aws-apigateway/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@
9292
"@aws-cdk/aws-s3-assets": "0.0.0",
9393
"@aws-cdk/core": "0.0.0",
9494
"@aws-cdk/cx-api": "0.0.0",
95-
"constructs": "^3.3.69"
95+
"constructs": "^3.3.69",
96+
"@aws-cdk/aws-stepfunctions": "0.0.0"
9697
},
9798
"homepage": "https://github.com/aws/aws-cdk",
9899
"peerDependencies": {
@@ -108,7 +109,8 @@
108109
"@aws-cdk/aws-s3-assets": "0.0.0",
109110
"@aws-cdk/core": "0.0.0",
110111
"@aws-cdk/cx-api": "0.0.0",
111-
"constructs": "^3.3.69"
112+
"constructs": "^3.3.69",
113+
"@aws-cdk/aws-stepfunctions": "0.0.0"
112114
},
113115
"engines": {
114116
"node": ">= 10.13.0 <13 || >=13.7.0"
@@ -318,6 +320,7 @@
318320
"attribute-tag:@aws-cdk/aws-apigateway.RequestAuthorizer.authorizerArn",
319321
"attribute-tag:@aws-cdk/aws-apigateway.TokenAuthorizer.authorizerArn",
320322
"attribute-tag:@aws-cdk/aws-apigateway.RestApi.restApiName",
323+
"attribute-tag:@aws-cdk/aws-apigateway.StepFunctionsRestApi.restApiName",
321324
"attribute-tag:@aws-cdk/aws-apigateway.SpecRestApi.restApiName",
322325
"attribute-tag:@aws-cdk/aws-apigateway.LambdaRestApi.restApiName",
323326
"from-method:@aws-cdk/aws-apigateway.Stage",

0 commit comments

Comments
 (0)