Skip to content

Commit

Permalink
StepFunctionsRestApi implemented along with unit and integration test…
Browse files Browse the repository at this point in the history
…ing.

Fixed Integration test and generated expected json for stepFunctionsRestApi Stack deployment.
closes aws#15081.
  • Loading branch information
Saqib Dhuka committed Oct 13, 2021
1 parent 263d0e7 commit 4ff8a66
Show file tree
Hide file tree
Showing 9 changed files with 1,060 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-apigateway/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export * from './authorizers';
export * from './access-log';
export * from './api-definition';
export * from './gateway-response';
export * from './stepFunctions-api';

// AWS::ApiGateway CloudFormation Resources:
export * from './apigateway.generated';
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-apigateway/lib/integrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './aws';
export * from './lambda';
export * from './http';
export * from './mock';
export * from './stepFunctions';
155 changes: 155 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/integrations/stepFunctions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
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;

}
/**
* 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);

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')" }
#else
$input.path('$.output')
#end`,
},
},
...errorResponse,
];

return integResponse;
}

function getRequestTemplates(handler: sfn.IStateMachine) {
const templateString = getTemplateString(handler);

const requestTemplate: { [contenType:string] : string } =
{
'application/json': templateString,
};

return requestTemplate;
}

function getTemplateString(handler: sfn.IStateMachine): string {
const templateString: string = `
#set($inputRoot = $input.path('$')) {
"input": "$util.escapeJavaScript($input.json(\'$\'))",
"stateMachineArn": "${handler.stateMachineArn}"
}`;

return templateString;
}
124 changes: 124 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/stepFunctions-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
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`).
*
* @default true
*/
readonly proxy?: boolean;

/**
* Rest API props options
* @default - no options.
*
*/
readonly options?: RestApiProps;
}

/**
* 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.options && props.options.defaultIntegration) || props.defaultIntegration) {
throw new Error('Cannot specify "defaultIntegration" since Step Functions integration is automatically defined');
}

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,
}),
...props.options,
...props,
});

this.root.addMethod('ANY', new StepFunctionsIntegration(props.handler, {
credentialsRole: apiRole,
}), {
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;
}
7 changes: 5 additions & 2 deletions packages/@aws-cdk/aws-apigateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@
"@aws-cdk/aws-s3-assets": "0.0.0",
"@aws-cdk/core": "0.0.0",
"@aws-cdk/cx-api": "0.0.0",
"constructs": "^3.3.69"
"constructs": "^3.3.69",
"@aws-cdk/aws-stepfunctions": "0.0.0"
},
"homepage": "https://github.com/aws/aws-cdk",
"peerDependencies": {
Expand All @@ -108,7 +109,8 @@
"@aws-cdk/aws-s3-assets": "0.0.0",
"@aws-cdk/core": "0.0.0",
"@aws-cdk/cx-api": "0.0.0",
"constructs": "^3.3.69"
"constructs": "^3.3.69",
"@aws-cdk/aws-stepfunctions": "0.0.0"
},
"engines": {
"node": ">= 10.13.0 <13 || >=13.7.0"
Expand Down Expand Up @@ -318,6 +320,7 @@
"attribute-tag:@aws-cdk/aws-apigateway.RequestAuthorizer.authorizerArn",
"attribute-tag:@aws-cdk/aws-apigateway.TokenAuthorizer.authorizerArn",
"attribute-tag:@aws-cdk/aws-apigateway.RestApi.restApiName",
"attribute-tag:@aws-cdk/aws-apigateway.StepFunctionsRestApi.restApiName",
"attribute-tag:@aws-cdk/aws-apigateway.SpecRestApi.restApiName",
"attribute-tag:@aws-cdk/aws-apigateway.LambdaRestApi.restApiName",
"from-method:@aws-cdk/aws-apigateway.Stage",
Expand Down
Loading

0 comments on commit 4ff8a66

Please sign in to comment.